././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/0000755000175100001770000000000014627677152013170 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534310.0 duecredit-0.10.2/CHANGELOG.md0000644000175100001770000005375414627677146015022 0ustar00runnerdocker# 0.10.2 (Tue Jun 04 2024) #### ๐Ÿ› Bug Fix - Do not bother testing with psychopy installed [#216](https://github.com/duecredit/duecredit/pull/216) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # 0.10.1 (Fri May 17 2024) #### ๐Ÿ› Bug Fix - Fix assertion placement + test on CI having numpy installed in a separate matrix run [#215](https://github.com/duecredit/duecredit/pull/215) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # 0.10.0 (Mon May 13 2024) #### ๐Ÿš€ Enhancement - Log to stderr not stdout by default [#213](https://github.com/duecredit/duecredit/pull/213) ([@yarikoptic](https://github.com/yarikoptic)) #### ๐Ÿ› Bug Fix - Update README.md [#212](https://github.com/duecredit/duecredit/pull/212) ([@marcelzwiers](https://github.com/marcelzwiers)) - add more type annotations [#209](https://github.com/duecredit/duecredit/pull/209) ([@a-detiste](https://github.com/a-detiste)) - Stop using distutils; test against Python 3.12 [#207](https://github.com/duecredit/duecredit/pull/207) ([@jwodder](https://github.com/jwodder)) - Remove use of importlib.metadata backport [#208](https://github.com/duecredit/duecredit/pull/208) ([@jwodder](https://github.com/jwodder)) - Add & apply pre-commit with black and isort [#199](https://github.com/duecredit/duecredit/pull/199) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) - Apply and conform to better linting [#197](https://github.com/duecredit/duecredit/pull/197) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) - raise, do not return some NotImplemented if type is other than what we can compare [#206](https://github.com/duecredit/duecredit/pull/206) ([@yarikoptic](https://github.com/yarikoptic)) - Stop executing `setup.py` [#198](https://github.com/duecredit/duecredit/pull/198) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) - Better coverage config [#200](https://github.com/duecredit/duecredit/pull/200) ([@jwodder](https://github.com/jwodder)) - Use Dependabot to update GitHub Actions versions [#201](https://github.com/duecredit/duecredit/pull/201) ([@jwodder](https://github.com/jwodder)) - Don't `pip install codecov` [#202](https://github.com/duecredit/duecredit/pull/202) ([@jwodder](https://github.com/jwodder)) - Remove mutable default argument [#196](https://github.com/duecredit/duecredit/pull/196) ([@jwodder](https://github.com/jwodder)) - misc. move typing improvements, WIP [#195](https://github.com/duecredit/duecredit/pull/195) ([@a-detiste](https://github.com/a-detiste)) - Drop python 3.6 and 3.7 and pyupgrade to 3.8 [#194](https://github.com/duecredit/duecredit/pull/194) ([@yarikoptic](https://github.com/yarikoptic)) - Improve type annotation of codebase, add CI for mypy, fix a string format in test [#191](https://github.com/duecredit/duecredit/pull/191) ([@a-detiste](https://github.com/a-detiste) [@yarikoptic](https://github.com/yarikoptic)) - This super simple PR allows programmers to more easily create (manage) pipeline specific .duecredit.p files / reports [#193](https://github.com/duecredit/duecredit/pull/193) ([@marcelzwiers](https://github.com/marcelzwiers)) #### โš ๏ธ Pushed to `master` - Revert "raise, do not return some NotImplemented if type is other than what we can compare" ([@yarikoptic](https://github.com/yarikoptic)) #### ๐Ÿ  Internal - [gh-actions](deps): Bump actions/setup-python from 4 to 5 [#210](https://github.com/duecredit/duecredit/pull/210) ([@dependabot[bot]](https://github.com/dependabot[bot])) - [gh-actions](deps): Bump codespell-project/actions-codespell from 1 to 2 [#203](https://github.com/duecredit/duecredit/pull/203) ([@dependabot[bot]](https://github.com/dependabot[bot])) - [gh-actions](deps): Bump actions/checkout from 2 to 4 [#204](https://github.com/duecredit/duecredit/pull/204) ([@dependabot[bot]](https://github.com/dependabot[bot])) - [gh-actions](deps): Bump actions/setup-python from 2 to 4 [#205](https://github.com/duecredit/duecredit/pull/205) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### ๐Ÿ“ Documentation - Add some more `cite()` information to the README.md [#192](https://github.com/duecredit/duecredit/pull/192) ([@marcelzwiers](https://github.com/marcelzwiers)) #### Authors: 5 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Alexandre Detiste ([@a-detiste](https://github.com/a-detiste)) - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Marcel Zwiers ([@marcelzwiers](https://github.com/marcelzwiers)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # 0.9.3 (Sun Nov 12 2023) #### ๐Ÿ› Bug Fix - ENH: codespell [#185](https://github.com/duecredit/duecredit/pull/185) ([@yarikoptic](https://github.com/yarikoptic)) #### ๐Ÿ  Internal - RF: remove remaining usage of SIX, and thus move away fully from Python 2 [#186](https://github.com/duecredit/duecredit/pull/186) ([@a-detiste](https://github.com/a-detiste) [@yarikoptic](https://github.com/yarikoptic)) - Ignore "vor" which is now detected as typo [#186](https://github.com/duecredit/duecredit/pull/186) ([@yarikoptic](https://github.com/yarikoptic)) #### ๐Ÿงช Tests - Use teardown_method instead of nose-y teardown [#190](https://github.com/duecredit/duecredit/pull/190) ([@yarikoptic](https://github.com/yarikoptic)) - Move from Travis to github workflow, disable coveralls [#187](https://github.com/duecredit/duecredit/pull/187) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - Alexandre Detiste ([@a-detiste](https://github.com/a-detiste)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # 0.9.2 (Wed Feb 01 2023) #### ๐Ÿ› Bug Fix - Tell LGTM to ignore unused imports in stub.py [#179](https://github.com/duecredit/duecredit/pull/179) ([@jwodder](https://github.com/jwodder)) #### ๐Ÿ  Internal - Make Debian Python version PEP440 compliant [#184](https://github.com/duecredit/duecredit/pull/184) ([@bdrung](https://github.com/bdrung)) #### Authors: 2 - Benjamin Drung ([@bdrung](https://github.com/bdrung)) - John T. Wodder II ([@jwodder](https://github.com/jwodder)) --- # 0.9.1 (Tue Apr 13 2021) #### ๐Ÿ› Bug Fix - Set up intuit auto to automate releases [#178](https://github.com/duecredit/duecredit/pull/178) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) - BF: make pypi upload happen in py 3.8 matrix (2.7 was removed) [#178](https://github.com/duecredit/duecredit/pull/178) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # [0.9.0](https://github.com/duecredit/duecredit/tree/0.9.0) (2021-04-13) - Drop support for Python < 3.6 - Python packaging is reworked, importlib-metadata is added as a dependency for python < 3.8 # [0.8.1](https://github.com/duecredit/duecredit/tree/0.8.0) (2021-01-26) - Announce lines with unescaped \ as r(aw) # [0.8.0](https://github.com/duecredit/duecredit/tree/0.8.0) (2020-02-09) - Variety of small fixes - Added .zenodo.json for more proper citation of duecredit - drop testing for 3.4 -- rare beast, lxml does not provide pkg for it - Support for citing matplotlib via injection - Address a few deprecation warnings (#146) - Provide more informative message whenever using older citeproc without encoding arg # [0.7.0](https://github.com/duecredit/duecredit/tree/0.7.0) (2019-03-01) - Prevent warnings from the injector's `__del__`. - InactiveDueCollector in `stub.py` now provides also `active=False` attribute (so external tools could directly query if duecredit is active) and no-op `activate` and `dump` for consistent API with a `due` object whenever `duecredit` is available. - Provide `Text` citation entry for free form text. It does not have any meaningful rendering in BibTex but is present in text rendering. `Url` entry also acquired text rendering with prefix `URL: `. # [0.6.5](https://github.com/duecredit/duecredit/tree/0.6.5) (2019-02-04) - Delay import of imports (thanks [Chris Markiewicz (@effigies)](https://github.com/effigies) - serves also as a workaround due to inconsistent installation of 3rd party modules/libraries such as openssl [\#142](https://github.com/duecredit/duecredit/issues/142) - Use https://doi.org as preferred DOI resolver. Thanks [Katrin Leinweber (@katrinleinweber)](https://github.com/katrinleinweber) for the contribution # [0.6.4](https://github.com/duecredit/duecredit/tree/0.6.4) (2018-06-25) - Added doi to numpy injection - Minor tune-ups to the docs # [0.6.3](https://github.com/duecredit/duecredit/tree/0.6.3) (2017-08-01) Fixed a bug disallowing installation of duecredit in environments with crippled/too-basic locale setting. # [0.6.2](https://github.com/duecredit/duecredit/tree/0.6.2) (2017-06-23) - Testing was converted to pytest - Various enhancements in supporting python3 and BiBTeX with utf-8 - New tag 'dataset' to describe datasets # [0.6.1](https://github.com/duecredit/duecredit/tree/0.6.1) (2016-07-09) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.6.0...0.6.1) **Merged pull requests:** - ENH: workaround for pages handling fixed in citeproc post 0.3.0 [\#98](https://github.com/duecredit/duecredit/pull/98) ([yarikoptic](https://github.com/yarikoptic)) # [0.6.0](https://github.com/duecredit/duecredit/tree/0.6.0) (2016-06-17) [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)* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/CONTRIBUTING.md0000644000175100001770000001573214627677134015431 0ustar00runnerdockerContributing 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 py.test -s -v duecredit ``` or similarly, ```sh py.test -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 pytest coverage coverage run --source duecredit -m py.test coverage report ### 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/LICENSE0000644000175100001770000000304114627677134014173 0ustar00runnerdockerCopyright 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/MANIFEST.in0000644000175100001770000000030514627677134014724 0ustar00runnerdockerinclude LICENSE README.md CHANGELOG.md requirements.txt tox.ini CONTRIBUTING.md setup.cfg include .coveragerc .travis.yml include examples/example_scipy.py graft duecredit global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/PKG-INFO0000644000175100001770000004073114627677152014272 0ustar00runnerdockerMetadata-Version: 2.1 Name: duecredit Version: 0.10.2 Summary: Publications (and donations) tracer Home-page: https://github.com/duecredit/duecredit Author: Yaroslav Halchenko, Matteo Visconti di Oleggio Castello Author-email: yoh@onerussian.com License: 2-clause BSD License Keywords: citation tracing Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Environment :: Other Environment Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Legal Industry Classifier: Intended Audience :: Other Audience Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Documentation Classifier: Topic :: Software Development :: Documentation Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: citeproc-py>=0.4 Requires-Dist: looseversion Requires-Dist: packaging Requires-Dist: requests Provides-Extra: tests Requires-Dist: pytest; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Requires-Dist: vcrpy; extra == "tests" Requires-Dist: contextlib2; extra == "tests" # duecredit [![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) [![DOI](https://zenodo.org/badge/DOI/110.5281/zenodo.3376260.svg)](https://doi.org/10.5281/zenodo.3376260) [![PyPI version fury.io](https://badge.fury.io/py/duecredit.svg)](https://pypi.python.org/pypi/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. ## Installation Duecredit is easy to install via pip, simply type: `pip install duecredit` ## Examples ### To cite the modules and methods you are using You can already start "registering" citations using duecredit in your Python modules and even registering citations (we call this approach "injections") for modules that do not (yet) use duecredit. duecredit will remain an optional dependency, i.e. your software will work correctly even without duecredit installed. For example, list citations of the modules and methods `yourproject` uses with a few simple commands: ```bash cd /path/to/yourmodule # for ~/yourproject cd yourproject # change directory into where the main code base is python -m duecredit yourproject.py ``` Or you can also display them in BibTex format, using: ```bash duecredit summary --format=bibtex ``` See this gif animation for a better illustration: ![Example](examples/duecredit_example.gif) ### To let others cite your software 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 a generic 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") By default, the added reference does not show up in the summary report (but see the `User-view` section below). If your reference is to a core package and you find that it should be listed in the summary then set `cite_module=True` (see [here](https://github.com/duecredit/duecredit/blob/master/duecredit/collector.py#L35) for a complete description of the arguments) due.cite(Doi("1.2.3/x.y.z"), description="The Answer to Everything", path="magicpy", cite_module=True) Similarly, to provide a direct reference for a function or a method, use the `dcite` decorator (by default this decorator sets cite_module=True) @due.dcite(Doi("1.2.3/x.y.z"), description="Resolves constipation issue") def pushit(): ... You can easily obtain a DOI for your software using Zenodo.org and a few other DOI providers. 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") ## Now what ### Do the due Once you obtained the references in the duecredit output, include them in in the references section of your paper or software. ### 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 the current directory or to your `DUECREDIT_FILE` environment setting: $> 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. ... ## Tags You are welcome to introduce new tags specific to your citations but we hope that for consistency across projects, you would use the following tags - `implementation` (default) โ€” an implementation of the cited method - `reference-implementation` โ€” the original implementation (ideally by the authors of the paper) of the cited method - `another-implementation` โ€” some other implementation of the method, e.g. if you would like to provide a citation for another implementation of the method you have implemented in your code and for which you have already provided `implementation` or `reference-implementation` tag - `use` โ€” publications demonstrating a worthwhile noting use of the method - `edu` โ€” tutorials, textbooks and other materials useful to learn more about cited functionality - `donate` โ€” should be commonly used with URL entries to point to the websites describing how to contribute some funds to the referenced project - `funding` โ€” to point to the sources of funding which provided support for a given functionality implementation and/or method development - `dataset` - for datasets ## 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 an 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 hand. **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. [citepy](https://github.com/clbarnes/citepy) -- Easily cite software libraries using information from automatically gathered from their package repository. ## Currently used by This is a running list of projects that use DueCredit natively. If you are using DueCredit, or plan to use it, please consider sending a pull request and add your project to this list. Thanks to [@fedorov](https://github.com/fedorov) for the idea. - [PyMVPA](http://www.pymvpa.org) - [fatiando](https://github.com/fatiando/fatiando) - [Nipype](https://github.com/nipy/nipype) - [QInfer](https://github.com/QInfer/python-qinfer) - [shablona](https://github.com/uwescience/shablona) - [gfusion](https://github.com/mvdoc/gfusion) - [pybids](https://github.com/INCF/pybids) - [Quickshear](https://github.com/nipy/quickshear) - [meqc](https://github.com/emdupre/meqc) - [MDAnalysis](https://www.mdanalysis.org) - [bctpy](https://github.com/aestrivex/bctpy) - [TorchIO](https://github.com/fepegar/torchio) - [BIDScoin](https://github.com/Donders-Institute/bidscoin) Last updated 2024-02-23. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/README.md0000644000175100001770000003600314627677134014451 0ustar00runnerdocker# duecredit [![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) [![DOI](https://zenodo.org/badge/DOI/110.5281/zenodo.3376260.svg)](https://doi.org/10.5281/zenodo.3376260) [![PyPI version fury.io](https://badge.fury.io/py/duecredit.svg)](https://pypi.python.org/pypi/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. ## Installation Duecredit is easy to install via pip, simply type: `pip install duecredit` ## Examples ### To cite the modules and methods you are using You can already start "registering" citations using duecredit in your Python modules and even registering citations (we call this approach "injections") for modules that do not (yet) use duecredit. duecredit will remain an optional dependency, i.e. your software will work correctly even without duecredit installed. For example, list citations of the modules and methods `yourproject` uses with a few simple commands: ```bash cd /path/to/yourmodule # for ~/yourproject cd yourproject # change directory into where the main code base is python -m duecredit yourproject.py ``` Or you can also display them in BibTex format, using: ```bash duecredit summary --format=bibtex ``` See this gif animation for a better illustration: ![Example](examples/duecredit_example.gif) ### To let others cite your software 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 a generic 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") By default, the added reference does not show up in the summary report (but see the `User-view` section below). If your reference is to a core package and you find that it should be listed in the summary then set `cite_module=True` (see [here](https://github.com/duecredit/duecredit/blob/master/duecredit/collector.py#L35) for a complete description of the arguments) due.cite(Doi("1.2.3/x.y.z"), description="The Answer to Everything", path="magicpy", cite_module=True) Similarly, to provide a direct reference for a function or a method, use the `dcite` decorator (by default this decorator sets cite_module=True) @due.dcite(Doi("1.2.3/x.y.z"), description="Resolves constipation issue") def pushit(): ... You can easily obtain a DOI for your software using Zenodo.org and a few other DOI providers. 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") ## Now what ### Do the due Once you obtained the references in the duecredit output, include them in in the references section of your paper or software. ### 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 the current directory or to your `DUECREDIT_FILE` environment setting: $> 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. ... ## Tags You are welcome to introduce new tags specific to your citations but we hope that for consistency across projects, you would use the following tags - `implementation` (default) โ€” an implementation of the cited method - `reference-implementation` โ€” the original implementation (ideally by the authors of the paper) of the cited method - `another-implementation` โ€” some other implementation of the method, e.g. if you would like to provide a citation for another implementation of the method you have implemented in your code and for which you have already provided `implementation` or `reference-implementation` tag - `use` โ€” publications demonstrating a worthwhile noting use of the method - `edu` โ€” tutorials, textbooks and other materials useful to learn more about cited functionality - `donate` โ€” should be commonly used with URL entries to point to the websites describing how to contribute some funds to the referenced project - `funding` โ€” to point to the sources of funding which provided support for a given functionality implementation and/or method development - `dataset` - for datasets ## 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 an 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 hand. **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. [citepy](https://github.com/clbarnes/citepy) -- Easily cite software libraries using information from automatically gathered from their package repository. ## Currently used by This is a running list of projects that use DueCredit natively. If you are using DueCredit, or plan to use it, please consider sending a pull request and add your project to this list. Thanks to [@fedorov](https://github.com/fedorov) for the idea. - [PyMVPA](http://www.pymvpa.org) - [fatiando](https://github.com/fatiando/fatiando) - [Nipype](https://github.com/nipy/nipype) - [QInfer](https://github.com/QInfer/python-qinfer) - [shablona](https://github.com/uwescience/shablona) - [gfusion](https://github.com/mvdoc/gfusion) - [pybids](https://github.com/INCF/pybids) - [Quickshear](https://github.com/nipy/quickshear) - [meqc](https://github.com/emdupre/meqc) - [MDAnalysis](https://www.mdanalysis.org) - [bctpy](https://github.com/aestrivex/bctpy) - [TorchIO](https://github.com/fepegar/torchio) - [BIDScoin](https://github.com/Donders-Institute/bidscoin) Last updated 2024-02-23. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.6011932 duecredit-0.10.2/duecredit/0000755000175100001770000000000014627677152015140 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/__init__.py0000644000175100001770000000140714627677134017253 0ustar00runnerdocker# 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 .dueswitch import due from .entries import BibTeX, Doi, Text, Url from .version import __release_date__, __version__ __all__ = ["Doi", "BibTeX", "Url", "Text", "due", "__version__", "__release_date__"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/__main__.py0000644000175100001770000000565314627677134017243 0ustar00runnerdocker# 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""" from __future__ import annotations import sys from . import __version__, due 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, global_ctx=None, local_ctx=None): if global_ctx is None: global_ctx = {} if local_ctx is None: local_ctx = {} try: exec(cmd, global_ctx, local_ctx) finally: # good opportunity to avoid atexit I guess. pass for now pass def main(argv=None): import getopt import os 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("{}: {}\n".format(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, _ 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 OSError as err: lgr.error("Cannot run file {!r} because: {}".format(sys.argv[0], err)) sys.exit(1) except SystemExit: pass if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.6011932 duecredit-0.10.2/duecredit/cmdline/0000755000175100001770000000000014627677152016553 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/__init__.py0000644000175100001770000000077714627677134020677 0ustar00runnerdocker# 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, cmd_test __all__ = ["cmd_summary", "cmd_test"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/cmd_summary.py0000644000175100001770000000357214627677134021454 0ustar00runnerdocker# 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. """ from __future__ import annotations import argparse import os import sys from ..config import DUECREDIT_FILE from ..io import BibTeXOutput, TextOutput from ..log import lgr __docformat__ = "restructuredtext" # magic line for manpage summary # man: -*- % summary of collected citations def setup_parser(parser: argparse.ArgumentParser) -> None: 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: argparse.Namespace) -> int: from ..io import PickleOutput if not os.path.exists(args.filename): lgr.debug("File {} doesn't exist. No summary available".format(args.filename)) return 1 due = PickleOutput.load(args.filename) # CollectorSummary(due).dump() out: TextOutput | BibTeXOutput 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() return 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/cmd_test.py0000644000175100001770000000162314627677134020731 0ustar00runnerdocker# 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""" from __future__ import annotations __docformat__ = "restructuredtext" import argparse # magic line for manpage summary # man: -*- % run unit-tests def setup_parser(parser: argparse.ArgumentParser) -> None: # TODO -- pass options such as verbosity etc pass def run(_args: argparse.Namespace) -> None: import duecredit # noqa: F401 raise NotImplementedError("Just use pytest duecredit for now") # duecredit.test() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/common_args.py0000644000175100001770000000255514627677134021440 0ustar00runnerdocker# 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 = ( # noqa: A001 "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""", ), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/helpers.py0000644000175100001770000001013614627677134020570 0ustar00runnerdocker# 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 __future__ import annotations __docformat__ = "restructuredtext" import argparse import re import sys from typing import Any, Pattern from ..utils import is_interactive class HelpAction(argparse.Action): def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, # noqa: U100 values, # noqa: U100 option_string: str | None = None, ) -> 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.sub( headpat, lambda match: rf"{match[1].upper()}{match[2]}:", helpstr ) # 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 = "{}\n{}".format(usagestr, helpstr[usage_length:]) print(helpstr) sys.exit(0) class LogLevelAction(argparse.Action): def __call__( self, parser: argparse.ArgumentParser, # noqa: U100 namespace: argparse.Namespace, # noqa: U100 values, option_string: str | None = None, # noqa: U100 ) -> None: from ..log import LoggerHelper LoggerHelper().set_level(level=values) def parser_add_common_args( parser: argparse.ArgumentParser, pos=None, opt=None, **kwargs: Any ) -> None: 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: argparse.ArgumentParser, opt, names=None, **kwargs ) -> None: 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: """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: str | None) -> Pattern[str] | None: if string: return re.compile(string) else: return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/cmdline/main.py0000644000175100001770000001665014627677134020061 0ustar00runnerdocker# 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 # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ """ from __future__ import annotations __docformat__ = "restructuredtext" import argparse import logging import os import sys import textwrap from typing import Any import duecredit.cmdline as duecmd from . import helpers from .. import __version__ from ..log import lgr from ..utils import setup_exceptionhook def _license_info() -> str: 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() -> list[str]: return sorted([c for c in dir(duecmd) if c.startswith("cmd_")]) def setup_parser() -> argparse.ArgumentParser: # 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=f"duecredit {__version__}\n\n{_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 "description" not 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=f"duecredit {cmd_name} {__version__}\n\n{_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: Any = None) -> 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("{} ({})".format(str(exc), exc.__class__.__name__)) sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/collector.py0000644000175100001770000004410714627677134017506 0ustar00runnerdocker# 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""" from __future__ import annotations from collections import namedtuple from functools import wraps import logging import os import sys from typing import TYPE_CHECKING, Any from .config import DUECREDIT_FILE from .entries import DueCreditEntry from .io import PickleOutput, TextOutput from .utils import borrowdoc, never_fail from .versions import external_versions lgr = logging.getLogger("duecredit.collector") if TYPE_CHECKING: from typing_extensions import Self CitationKey = namedtuple("CitationKey", ["path", "entry_key"]) class Citation: """Encapsulates citations and information on their use""" def __init__( self, entry: DueCreditEntry, description: str | None = None, path: str | None = None, version: None | str | tuple[str, str] = None, cite_module: bool = False, tags: list[str] | None = None, ): """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. (see full list in README.md or https://github.com/duecredit/duecredit/#tags) - "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 if tags is None: tags = ["implementation"] self.tags = tags or [] self.version = version self.count = 0 def __repr__(self) -> str: argl = [repr(self._entry)] if self._description: argl.append(f"description={repr(self._description)}") if self._path: argl.append(f"path={repr(self._path)}") if self._cite_module: argl.append(f"cite_module={repr(self._cite_module)}") if argl: args = ", ".join(argl) else: args = "" return self.__class__.__name__ + f"({args})" @property def path(self) -> str: return self._path @path.setter def path(self, path: str) -> None: # TODO: verify value, if we are not up for it -- just make _path public self._path = path @property def entry(self) -> DueCreditEntry: return self._entry @property def description(self) -> str | None: return self._description @property def cite_module(self) -> bool: return self._cite_module @property def cites_module(self) -> bool | None: if not self.path: return None return not (":" in self.path) @property def module(self) -> str | None: if not self.path: return None return self.path.split(":", 1)[0] @property def package(self) -> str | None: module = self.module if not module: return None return module.split(".", 1)[0] @property def objname(self) -> str | None: 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: Self) -> bool: """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) -> CitationKey: return CitationKey(self.path, self.entry.key) @staticmethod def get_key(path: str, entry_key: str) -> CitationKey: return CitationKey(path, entry_key) def set_entry(self, newentry: DueCreditEntry) -> None: self._entry = newentry class DueCreditCollector: """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: dict[str, DueCreditEntry] | None = None, citations: dict[CitationKey, Citation] | None = None, ) -> None: self._entries = entries or {} self.citations = citations or {} @never_fail def add(self, entry: DueCreditEntry | list[DueCreditEntry]) -> None: """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: str) -> None: """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: str) -> None: 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: DueCreditEntry | str, **kwargs: Any) -> Citation: # 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 isinstance(citation, Citation) 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 self.citations.values(): # if citation.package == package \ # and not citation.version: version = external_versions[package] citation.version = version return citation def _citations_fromentrykey(self) -> dict[str, Citation]: """Return a dictionary with the current citations indexed by the entry key""" citations_key = dict() for (_path, entry_key), citation in self.citations.items(): if entry_key not in citations_key: citations_key[entry_key] = citation return citations_key @staticmethod def _args_match_conditions( conditions: dict[Any, Any], *fargs: Any, **fkwargs: Any ) -> bool: """Helper to identify when to trigger citation given parameters to the function call""" for (argpos, kwarg), values in conditions.items(): # 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: ... 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"] = "{}:{}".format(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 {} within module {}".format(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 ): self.cite(*args, **kwargs) except Exception as e: lgr.warning("Failed to cite due to {}".format(e)) return func(*fargs, **fkwargs) cite_wrapper.__duecredited__ = func return cite_wrapper return func_wrapper @never_fail def __repr__(self) -> str: argl = [] if self.citations: argl.append(f"citations={repr(self.citations)}") if self._entries: argl.append(f"entries={repr(self._entries)}") if argl: args = ", ".join(argl) else: args = "" return self.__class__.__name__ + f"({args})" @never_fail def __str__(self) -> str: return self.__class__.__name__ + " {:d} entries, {:d} citations".format( len(self._entries), len(self.citations) ) # TODO: redo heavily -- got very messy class CollectorSummary: """A helper which would take care about exporting citations upon its Death""" def __init__( self, collector: DueCreditCollector, outputs: str = "stdout,pickle", fn: str = DUECREDIT_FILE, ) -> None: 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_: str, collector: DueCreditCollector, fn: str | None = None ) -> TextOutput | PickleOutput: # 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) -> None: for output in self._outputs: output.dump() # TODO: provide HTML, MD, RST etc output formats ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/config.py0000644000175100001770000000131414627677134016756 0ustar00runnerdocker# 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 = os.getenv("DUECREDIT_FILE") or ".duecredit.p" # NB: `or` catches empty env var. TODO: Add file name/ext check for the env # variable? ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/dueswitch.py0000644000175100001770000001161514627677134017515 0ustar00runnerdocker# 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 """ from __future__ import annotations import atexit import os from typing import TYPE_CHECKING, Any from .log import lgr from .utils import never_fail if TYPE_CHECKING: from duecredit.collector import DueCreditCollector from .stub import InactiveDueCreditCollector def _get_duecredit_enable() -> bool: 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() -> InactiveDueCreditCollector: from .stub import InactiveDueCreditCollector return InactiveDueCreditCollector() @never_fail def _get_active_due() -> DueCreditCollector | InactiveDueCreditCollector: from duecredit.collector import DueCreditCollector from .config import DUECREDIT_FILE from .io import load_due # 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: 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: """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: bool = False) -> None: self.__active: bool | None = None self.__collectors = {False: inactive, True: active} self.__activations_done = False if not (inactive and active): raise ValueError( "Both inactive and active collectors should be provided. " "Got active=%r, inactive=%r" % (active, inactive) ) self.activate(activate) @property def active(self): return self.__active @never_fail def dump(self, **kwargs: Any) -> None: """Dumps summary of the citations Parameters ---------- **kwargs: dict Passed to `CollectorSummary` constructor. """ from duecredit.collector import CollectorSummary due_summary = CollectorSummary(self.__collectors[True], **kwargs) due_summary.dump() def __prepare_exit_and_injections(self) -> None: # Wrapper to create and dump summary... passing method doesn't work: # probably removes instance too early atexit.register(self.dump) # Deal with injector from .injections import DueCreditInjector injector = DueCreditInjector(collector=self.__collectors[True]) injector.activate() @never_fail def activate(self, activate: bool = True) -> None: # 1st step -- if activating/deactivating switch between the two collectors if self.__active is not activate: # we need to switch the state # InactiveDueCollector also has those special methods defined # in DueSwitch so that client code could query/call (for no effect). # So we shouldn't delete or bind them either def is_public_or_special(x): return not (x.startswith("_") or x in ("activate", "active", "dump")) # Clean up current bindings first for k in filter(is_public_or_special, dir(self)): delattr(self, k) new_due = self.__collectors[activate] for k in filter(is_public_or_special, 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()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/entries.py0000644000175100001770000000510514627677134017164 0ustar00runnerdocker# 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 # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from __future__ import annotations import logging import re lgr = logging.getLogger("duecredit.entries") class DueCreditEntry: _key: str def __init__(self, rawentry: str, key: str | None = None) -> None: self._rawentry = rawentry self._key = key or rawentry.lower() def __eq__(self, other: object) -> bool: if not isinstance(other, DueCreditEntry): return NotImplemented return (self._rawentry == other._rawentry) and (self._key == other._key) def get_key(self) -> str: return self._key @property def key(self) -> str: return self.get_key() @property def rawentry(self) -> str: return self._rawentry def _process_rawentry(self) -> None: pass def __repr__(self) -> str: argl = [repr(self._rawentry), f"key={repr(self._key)}"] args = ", ".join(argl) return self.__class__.__name__ + f"({args})" def format(self) -> str: # TODO: return nice formatting of the entry return str(self._rawentry) class BibTeX(DueCreditEntry): def __init__(self, bibtex: str, key: str | None = None) -> None: super().__init__(bibtex.strip()) self._reference = None self._process_rawentry() if key is not None: # use the one provided, not the parsed one lgr.debug( "Replacing parsed key %s for BibTeX with the provided %s", self._key, key, ) self._key = key def _process_rawentry(self) -> None: reg = re.match( r"\s*@(?P\S*)\s*\{\s*(?P\S*)\s*,.*", self._rawentry, flags=re.MULTILINE, ) assert reg matches = reg.groupdict() self._key = matches["key"] class Text(DueCreditEntry): """Just a free text entry without any special super powers in rendering etc""" pass # nothing special I guess class Doi(DueCreditEntry): @property def doi(self) -> str: return self._rawentry class Url(DueCreditEntry): @property def url(self) -> str: return self._rawentry ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.6051931 duecredit-0.10.2/duecredit/injections/0000755000175100001770000000000014627677152017305 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/__init__.py0000644000175100001770000000107514627677134021421 0ustar00runnerdocker# 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 __all__ = ["DueCreditInjector"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/injector.py0000644000175100001770000004370114627677134021501 0ustar00runnerdocker# 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 """ from __future__ import annotations __docformat__ = "restructuredtext" import builtins as __builtin__ from functools import wraps from glob import glob import logging import os from os.path import basename, dirname from os.path import join as pathjoin import sys from typing import TYPE_CHECKING, Any from ..log import lgr if TYPE_CHECKING: from ..entries import BibTeX, Doi, Url __all__ = ["DueCreditInjector", "find_object"] # TODO: move elsewhere def _short_str(obj: Any, lng: int = 30) -> str: """Return a shortened str of an object -- for logging""" s = str(obj) if len(s) > lng: return s[: lng - 3] + "..." else: return s def get_modules_for_injection() -> list[str]: """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: Any, path: str) -> tuple[Any, str, Any]: """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: """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) -> None: lgr.log( 2, "Reassigning _orig_import from %r to %r", DueCreditInjector.__orig_import, value, ) DueCreditInjector.__orig_import = value def __init__(self, collector=None) -> None: if collector is None: from duecredit import due collector = due self._collector = collector self._delayed_injections: dict[str, str] = {} self._entry_records: dict[ str, dict[str | None, Any] ] = {} # dict: modulename: {object: [('entry', cite kwargs)]} self._processed_modules: set[str] = 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[str] = set() self.__processing_queue = False self._active = False lgr.debug("Created injector %r", self) def _populate_delayed_injections(self) -> None: 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: str, obj: str | None, entry: 'Doi' | 'BibTeX' | 'Url', min_version=None, max_version=None, **kwargs: Any, ) -> None: """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) -> str: return "." * self.__import_level def _process_delayed_injection(self, mod_name: str) -> None: 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 {}: {!r}".format(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: str) -> None: """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 self._entry_records[mod_name].items(): 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 {} in module {}: {}".format(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! assert obj_name 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: str, *args: Any, **kwargs: Any) -> Any: 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: str, import_level_prefix: str, level) -> None: """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) -> None: """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) -> None: 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) -> None: if lgr: lgr.debug("%s is asked to be deleted", self) try: if self._active: self.deactivate() except: # noqa: E722, B001 pass try: super(self.__class__, self).__del__() # type: ignore except: # noqa: E722, B001 pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_biosig.py0000644000175100001770000000170514627677134021775 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: injector.add( "biosig", None, Doi("10.1109/MC.2008.407"), description="I/O library for biosignal data formats", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_dipy.py0000644000175100001770000000207014627677134021462 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: # 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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_matplotlib.py0000644000175100001770000000335714627677134022675 0ustar00runnerdocker# 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 matplotlib module """ from __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX, Doi # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector bib_str = """ @Article{Hunter:2007, Author = {Hunter, J. D.}, Title = {Matplotlib: A 2D graphics environment}, Journal = {Computing in Science \\& Engineering}, Volume = {9}, Number = {3}, Pages = {90--95}, abstract = {Matplotlib is a 2D graphics package used for Python for application development, interactive scripting, and publication-quality image generation across user interfaces and operating systems.}, publisher = {IEEE COMPUTER SOC}, doi = {10.1109/MCSE.2007.55}, year = 2007 } """.strip() def inject(injector: DueCreditInjector) -> None: injector.add( "matplotlib", None, BibTeX(bib_str), description="Plotting with Python", tags=["implementation"], ) doi_prefix = "10.5281/zenodo." # latest version injector.add( "matplotlib", None, Doi(doi_prefix + "2893252"), description="Plotting with Python", tags=["implementation"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_mdp.py0000644000175100001770000001167514627677134021310 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX, Doi # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_mne.py0000644000175100001770000000234514627677134021301 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: # 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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_nibabel.py0000644000175100001770000000202614627677134022112 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: injector.add( "nibabel", None, Doi("10.5281/zenodo.60847"), cite_module=True, description="I/O library to access to common neuroimaging file formats", tags=["implementation"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_nipy.py0000644000175100001770000000263314627677134021501 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_nipype.py0000644000175100001770000000547714627677134022037 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX, Doi # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: # 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']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_numpy.py0000644000175100001770000000263614627677134021675 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: injector.add( "numpy", None, BibTeX( r""" @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}, doi={10.1109/MCSE.2011.37} } """ ), tags=["implementation"], cite_module=True, description="Scientific tools library", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_pandas.py0000644000175100001770000000254314627677134021770 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_psychopy.py0000644000175100001770000000232214627677134022373 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_scipy.py0000644000175100001770000001530614627677134021652 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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( r""" @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( r""" @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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_skimage.py0000644000175100001770000000201014627677134022127 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING 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 if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: # http://scikit-image.org injector.add( "skimage", None, Doi("10.7717/peerj.453"), description="scikit-image: Image processing in Python.", tags=["implementation"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/injections/mod_sklearn.py0000644000175100001770000001237114627677134022161 0ustar00runnerdocker# 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 __future__ import annotations from typing import TYPE_CHECKING from ..entries import BibTeX, Doi, Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None if TYPE_CHECKING: from .injector import DueCreditInjector def inject(injector: DueCreditInjector) -> None: 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"], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/io.py0000644000175100001770000003506414627677134016131 0ustar00runnerdocker# 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 __future__ import annotations # 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") from collections import defaultdict import locale from os.path import dirname, exists import pickle import re import tempfile import time from typing import TYPE_CHECKING, Any import warnings from packaging.version import Version from .config import CACHE_DIR, DUECREDIT_FILE from .entries import BibTeX, Doi, DueCreditEntry, Text, Url from .log import lgr from .versions import external_versions if TYPE_CHECKING: from .collector import Citation _PREFERRED_ENCODING = locale.getpreferredencoding() def get_doi_cache_file(doi: str) -> str: # where to cache bibtex entries if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) return os.path.join(CACHE_DIR, doi) def import_doi(doi: str, sleep: float = 0.5, retries: int = 10) -> str: import requests cached = get_doi_cache_file(doi) if exists(cached): with open(cached) as f: doi = f.read() return doi # else -- fetch it headers = {"Accept": "application/x-bibtex; charset=utf-8"} url = "https://doi.org/" + doi while retries > 0: lgr.debug("Submitting GET to %s with headers %s", url, headers) 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: f.write(bibtex) return bibtex def _is_contained(toppath: str, subpath: str) -> bool: if ":" not in toppath: return ( (toppath == subpath) or (subpath.startswith(toppath + ".")) or (subpath.startswith(toppath + ":")) ) else: return subpath.startswith(toppath + ".") class Output: """A generic class for setting up citations that then will be outputted differently (e.g., Bibtex, Text, etc.)""" def __init__(self, fd, collector) -> None: self.fd = fd self.collector = collector def _get_collated_citations( self, tags: list[str] | None = None, all_: bool | None = None ) -> tuple[ dict[str, list[Citation]], dict[str, list[Citation]], dict[str, list[Citation]] ]: """Given all the citations, filter only those that the user wants and those that were actually used""" if not tags: env = os.environ.get( "DUECREDIT_REPORT_TAGS", "reference-implementation,implementation,dataset", ) assert type(env) is str tags = env.split(",") if all_ is None: # consult env var env = os.environ.get("DUECREDIT_REPORT_ALL", "").lower() assert type(env) is str all_ = env in {"1", "true", "yes", "on"} tagset = set(tags) citations = self.collector.citations if tagset != {"*"}: # Filter out citations based on tags citations = { k: c for k, c in citations.items() if tagset.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 citations.items(): 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) # type: ignore ) or any( filter(lambda x: _is_contained(package, x), cited_modobj) # type: ignore ) ): continue else: # we don't need it del packages[package] return packages, modules, objects def dump(self, tags=None) -> None: raise NotImplementedError class TextOutput(Output): def __init__(self, fd, collector, style=None) -> None: super().__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) -> str: descriptions = map(str, {str(r.description) for r in citations}) versions = map(str, {str(r.version) for r in citations}) refnrs = map(str, [citation_nr[c.entry.key] for c in citations]) path = citations[0].path return "- {} / {} (v {}) [{}]\n".format( ", ".join(descriptions), path, ", ".join(versions), ", ".join(refnrs) ) def dump(self, tags=None) -> 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 cites = pmo[path] if ":" in path or "." in path: self.fd.write(" ") self.fd.write(self._format_citations(cites, citation_nr)) start_refnr += len(cites) # Print out some stats stats = [ (len(packages), "package"), (len(modules), "module"), (len(objects), "function"), ] for n, cit_type in stats: self.fd.write( "\n{} {} 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]: # 'import Citation / assert type(cit) is Citation' would pollute environment ek = cit.entry.key # type: ignore if ek not in printed_keys: self.fd.write(f"\n[{citation_nr[ek]}] ") self.fd.write(get_text_rendering(cit.entry, style=self.style)) printed_keys.append(ek) self.fd.write("\n") def get_text_rendering(entry: DueCreditEntry, style: str = "harvard1") -> str: if isinstance(entry, Doi): return format_bibtex(get_bibtex_rendering(entry), style=style) elif isinstance(entry, BibTeX): return format_bibtex(entry, style=style) elif isinstance(entry, Text): return entry.format() elif isinstance(entry, Url): return f"URL: {entry.format()}" else: return str(entry) def get_bibtex_rendering(entry: DueCreditEntry) -> BibTeX: 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 condition_bibtex(bibtex: str) -> bytes: """Given a bibtex entry, "condition" it for processing with citeproc Primarily a set of workarounds for either non-standard BibTeX entries or citeproc bugs """ # 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("\u2013", "--") + "\n" # workaround for citeproc 0.3.0 failing to parse a single page pages field # as for BIDS paper. Workaround to add trailing + after pages number # related issue asking for a new release: https://github.com/brechtm/citeproc-py/issues/72 bibtex = re.sub(r'(pages\s*=\s*["{]\d+)(["}])', r"\1+\2", bibtex) # partial workaround for citeproc failing to parse page numbers when they contain non-numeric characters # remove opening letter, e.g. 'S123' -> '123' # related issue: https://github.com/brechtm/citeproc-py/issues/74 bibtex = re.sub(r'(pages\s*=\s*["{])([a-zA-Z])', r"\g<1>", bibtex) return bibtex.encode("utf-8") def format_bibtex(bibtex_entry: BibTeX, style: str = "harvard1") -> str: try: import citeproc as cp from citeproc.source.bibtex import BibTeX as cpBibTeX 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) ) decode_exceptions: tuple[type[Exception], ...] try: from citeproc.source.bibtex.bibparse import BibTeXDecodeError decode_exceptions = (UnicodeDecodeError, BibTeXDecodeError) except ImportError: # this version doesn't yet have this exception defined decode_exceptions = (UnicodeDecodeError,) key = bibtex_entry.get_key() # need to save it temporarily to use citeproc-py fname = tempfile.mktemp(suffix=".bib") try: with open(fname, "wb") as f: f.write(condition_bibtex(bibtex_entry.rawentry)) # We need to avoid cpBibTex spitting out warnings old_filters = warnings.filters[:] # store a copy of filters warnings.simplefilter("ignore", UserWarning) try: try: bib_source = cpBibTeX(fname) except decode_exceptions: # So .bib must be having UTF-8 characters. With # a recent (not yet released past v0.3.0-68-g9800dad # we should be able to provide encoding argument bib_source = cpBibTeX(fname, encoding="utf-8") except Exception as e: msg = "Failed to process BibTeX file {}: {}.".format(fname, e) if "unexpected keyword argument" in str(e): citeproc_version = external_versions["citeproc"] if isinstance(citeproc_version, Version) and citeproc_version < Version( "0.4" ): err = "need a newer citeproc-py >= 0.4.0" msg += " You might just " + err else: err = str(e) lgr.error(msg) return "ERRORED: %s" % err 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"): for i in range(50): try: os.unlink(fname) except OSError: if i < 49: time.sleep(0.1) continue else: raise break 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: def __init__(self, collector, fn=DUECREDIT_FILE) -> None: self.collector = collector self.fn = fn def dump(self) -> None: with open(self.fn, "wb") as f: pickle.dump(self.collector, f) @classmethod def load(cls, filename: str = DUECREDIT_FILE) -> Any: with open(filename, "rb") as f: return pickle.load(f) class BibTeXOutput(Output): def __init__(self, fd, collector) -> None: super().__init__(fd, collector) def dump(self, tags=None) -> 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 Exception: lgr.warning("Failed to generate BibTeX for %s", entry) continue self.fd.write(bibtex.rawentry + "\n") def load_due(filename: str) -> Any: return PickleOutput.load(filename) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/log.py0000644000175100001770000002052714627677134016301 0ustar00runnerdocker# 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. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from __future__ import annotations import logging import logging.handlers import os from os.path import basename, dirname import platform import re import sys import traceback from .utils import is_interactive __all__ = ["ColorFormatter", "lgr"] # Snippets from traceback borrowed from PyMVPA upstream/2.4.0-39-g69ad545 MIT license def mbasename(s: str) -> str: """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 {"base", "__init__"}: base = basename(dirname(s)) + "." + base return base class TraceBack: """Customized traceback to be included in debug messages""" def __init__(self, collide: bool = False) -> None: """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) -> str: 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(["{}:{}".format(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: bool | None = None, log_name: bool = False) -> None: 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: bool = False) -> str: 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: str, use_color: bool = False) -> str: 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: logging.LogRecord) -> str: 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: """Helper to establish and control a Logger""" def __init__(self, name: str = "duecredit") -> None: self.name = name self.lgr = logging.getLogger(name) def _get_environ( self, var: str, default: str | None | bool = None ) -> str | None | bool: return os.environ.get(self.name.upper() + "_%s" % var.upper(), default) def set_level(self, level: str | None = None, default: str = "WARNING") -> None: """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 lv = self._get_environ("LOGLEVEL") assert type(lv) is not bool level = lv 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: str | None = None) -> logging.Logger: """Initialize and return the logger Parameters ---------- target: string, optional Which log target to request logger for logtarget: { 'stderr', 'stdout', 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 "stderr") assert type(logtarget_) is str # 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 log_name = self._get_environ("LOGNAME", False) assert type(log_name) is bool loghandler.setFormatter(ColorFormatter(use_color=use_color, log_name=log_name)) # 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/parsers.py0000644000175100001770000000154614627677134017177 0ustar00runnerdockerdef extract_references_from_rst(rst: str) -> None: # 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() -> None: # 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]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/stub.py0000644000175100001770000000421314627677134016467 0ustar00runnerdocker# emacs: at the end of the file # ex: set sts=4 ts=4 sw=4 et: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # from __future__ import annotations from typing import Any """ 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, Text See https://github.com/duecredit/duecredit/blob/master/README.md for examples. Origin: Originally a part of the duecredit Copyright: 2015-2021 DueCredit developers License: BSD-2 """ __version__ = "0.0.9" class InactiveDueCreditCollector: """Just a stub at the Collector which would not do anything""" def _donothing(self, *_args: Any, **_kwargs: Any) -> None: """Perform no good and no bad""" pass def dcite(self, *_args: Any, **_kwargs: Any): """If I could cite I would""" def nondecorating_decorator(func): return func return nondecorating_decorator active = False activate = add = cite = dump = load = _donothing def __repr__(self) -> str: return self.__class__.__name__ + "()" def _donothing_func(*args: Any, **kwargs: Any) -> None: """Perform no good and no bad""" pass try: from duecredit import BibTeX, Doi, Text, Url, due # lgtm [py/unused-import] if "due" in locals() and not hasattr(due, "cite"): raise RuntimeError("Imported due lacks .cite. DueCredit is now disabled") except Exception as e: if not isinstance(e, ImportError): import logging logging.getLogger("duecredit").error( "Failed to import duecredit due to %s" % str(e) ) # Initiate due stub due = InactiveDueCreditCollector() # type: ignore BibTeX = Doi = Url = Text = _donothing_func # type: ignore # Emacs mode definitions # Local Variables: # mode: python # py-indent-offset: 4 # tab-width: 4 # indent-tabs-mode: nil # End: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.6051931 duecredit-0.10.2/duecredit/tests/0000755000175100001770000000000014627677152016302 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/__init__.py0000644000175100001770000000000014627677134020401 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.5971932 duecredit-0.10.2/duecredit/tests/envs/0000755000175100001770000000000014627677152017255 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717534313.6051931 duecredit-0.10.2/duecredit/tests/envs/nolxml/0000755000175100001770000000000014627677152020566 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/envs/nolxml/lxml.py0000644000175100001770000000011614627677134022112 0ustar00runnerdockerraise ImportError("Artificially failining import as if no lxml is available") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/duecredit/tests/mod/0000755000175100001770000000000014627677152017061 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/mod/__init__.py0000644000175100001770000000021014627677134021163 0ustar00runnerdocker"""Module to test various duecredit functionality, e.g. injections""" from .imported import * # noqa: F401, F403 __version__ = "0.5" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/mod/imported.py0000644000175100001770000000122614627677134021257 0ustar00runnerdockerfrom typing import Any def testfunc1(arg1: Any, kwarg1: Any = None) -> str: """custom docstring""" return "testfunc1: {}, {}".format(arg1, kwarg1) class TestClass1: """wrong custom docstring""" def testmeth1(self, arg1: Any, kwarg1: Any = None) -> str: """custom docstring""" return "TestClass1.testmeth1: {}, {}".format(arg1, kwarg1) class TestClass12: """wrong custom docstring""" class Embed: """wrong custom docstring""" def testmeth1(self, arg1: Any, kwarg1: Any = None) -> str: """custom docstring""" return "TestClass12.Embed.testmeth1: {}, {}".format(arg1, kwarg1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/mod/submod.py0000644000175100001770000000047714627677134020734 0ustar00runnerdocker"""Some test submodule""" from typing import Any def testfunc(arg1: Any, kwarg1: Any = None) -> str: """testfunc docstring""" return "testfunc: {}, {}".format(arg1, kwarg1) class TestClass: def testmeth(self, arg1: Any, kwarg1: Any = None) -> str: return "testmeth: {}, {}".format(arg1, kwarg1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test__main__.py0000644000175100001770000000366114627677134021302 0ustar00runnerdocker# 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 __future__ import annotations from io import StringIO import sys from typing import TYPE_CHECKING import pytest from pytest import MonkeyPatch from .. import __main__, __version__, due if TYPE_CHECKING: import py def test_main_help(monkeypatch: MonkeyPatch) -> None: # Patch stdout fakestdout = StringIO() monkeypatch.setattr(sys, "stdout", fakestdout) pytest.raises(SystemExit, __main__.main, ["__main__.py", "--help"]) assert fakestdout.getvalue().startswith( "Usage: %s -m duecredit [OPTIONS] [ARGS]\n" % sys.executable ) def test_main_version(monkeypatch: MonkeyPatch) -> None: # Patch stdout fakestdout = StringIO() monkeypatch.setattr(sys, "stdout", fakestdout) pytest.raises(SystemExit, __main__.main, ["__main__.py", "--version"]) assert fakestdout.getvalue().rstrip() == "duecredit %s" % __version__ def test_main_run_a_script(tmpdir: py.path.local, monkeypatch: MonkeyPatch) -> None: tempfile = str(tmpdir.mkdir("sub").join("tempfile.txt")) content = b'print("Running the script")\n' with open(tempfile, "wb") as f: f.write(content) # Patch stdout fakestdout = StringIO() monkeypatch.setattr(sys, "stdout", fakestdout) # Patch due.activate count = [0] def count_calls(*_args, **_kwargs): count[0] += 1 monkeypatch.setattr(due, "activate", count_calls) __main__.main(["__main__.py", tempfile]) assert fakestdout.getvalue().rstrip() == "Running the script" # And we have "activated" the due assert count[0] == 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_api.py0000644000175100001770000001751114627677134020471 0ustar00runnerdocker# 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 __future__ import annotations from collections.abc import Iterator import os from os.path import dirname from os.path import join as pathjoin import shutil from subprocess import PIPE, Popen import sys import tempfile from typing import overload import pytest from pytest import MonkeyPatch from duecredit.collector import DueCreditCollector from duecredit.entries import BibTeX, Doi from duecredit.stub import InactiveDueCreditCollector from ..utils import on_windows # temporary location where stuff would be copied badlxml_path = pathjoin(dirname(__file__), "envs", "nolxml") stubbed_dir = tempfile.mktemp() stubbed_script = pathjoin(pathjoin(stubbed_dir, "script.py")) @pytest.fixture(scope="module") def stubbed_env() -> Iterator[str]: """Create stubbed module with a sample script""" os.makedirs(stubbed_dir) with open(stubbed_script, "wb") as f: f.write( b""" from due import due, Doi kwargs = dict( entry=Doi("10.1007/s12021-008-9041-y"), description="Multivariate pattern analysis of neural data", tags=["use"] ) due.cite(path="test", **kwargs) @due.dcite(**kwargs) def method(arg): return arg+1 assert method(1) == 2 print("done123") """ ) # copy stub.py under stubbed shutil.copy( pathjoin(dirname(__file__), os.pardir, "stub.py"), pathjoin(stubbed_dir, "due.py"), ) yield stubbed_script # cleanup shutil.rmtree(stubbed_dir) @pytest.mark.parametrize( "collector_class", [DueCreditCollector, InactiveDueCreditCollector] ) def test_api(collector_class) -> None: due = collector_class() # 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") # Cite 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: # Conception process is usually way too easy to be referenced def __init__(self) -> None: pass # including functionality within/by the methods @due.dcite("XXX00") def birth(self, _gender) -> str: return "Rachel was born" kid = Child() kid.birth("female") @overload def run_python_command(cmd: str): ... @overload def run_python_command(cmd: None, script: str): ... def run_python_command(cmd: str | None = None, script: str | None = None): """Just a tiny helper which runs command and returns exit code, stdout, stderr""" if script is None: assert cmd is not None args = ["-c", cmd] else: assert cmd is None args = [script] try: # run script from some temporary directory so we do not breed .duecredit.p # in current directory tmpdir = tempfile.mkdtemp() python = Popen([sys.executable] + args, stdout=PIPE, stderr=PIPE, cwd=tmpdir) stdout, stderr = python.communicate() # wait() ret = python.poll() finally: shutil.rmtree(tmpdir) # TODO stdout cannot decode on Windows special character /x introduced return ret, stdout.decode(errors="ignore"), stderr.decode() # 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(monkeypatch: MonkeyPatch) -> None: if on_windows: pytest.xfail("Fails for some reason on Windows") monkeypatch.setitem( os.environ, "PYTHONPATH", "{}:{}".format(badlxml_path, os.environ.get("PYTHONPATH", "")), ) ret, out, err = run_python_command("import lxml") assert ret == 1 assert "ImportError" in err ret, out, err = run_python_command("import duecredit") if "CoverageWarning" not in err: # TODO: deal with that warning # "--include is ignored because --source is set" assert err == "" assert out == "" assert ret == 0 @pytest.mark.parametrize( "env", [ {}, {"DUECREDIT_ENABLE": "yes"}, {"DUECREDIT_ENABLE": "yes", "DUECREDIT_REPORT_TAGS": "*"}, {"DUECREDIT_TEST_EARLY_IMPORT_ERROR": "yes"}, ], ) @pytest.mark.parametrize( "kwargs", [ # direct command to evaluate {"cmd": 'import duecredit; import numpy as np; print("done123")'}, # script with decorated funcs etc -- should be importable {"script": stubbed_script}, ], ) def test_noincorrect_import_if_no_lxml_numpy( monkeypatch: MonkeyPatch, kwargs, env, stubbed_env # noqa: U100 ) -> None: # 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 # noqa: F401 except ImportError: pytest.skip("We need to have numpy to test correct operation") fake_env_nolxml_ = { "PYTHONPATH": "{}:{}".format(badlxml_path, os.environ.get("PYTHONPATH", "")) }.copy() fake_env_nolxml_.update(env) for key in fake_env_nolxml_: monkeypatch.setitem(os.environ, key, fake_env_nolxml_[key]) ret, out, err = run_python_command(**kwargs) direct_duecredit_import = "import duecredit" in kwargs.get("cmd", "") if direct_duecredit_import and env.get("DUECREDIT_TEST_EARLY_IMPORT_ERROR", ""): # We do fail then upon regular import but stubbed script should be ok # since should use the stub assert "Both inactive and active collectors should be provided" in err assert ret == 1 else: # TODO: fixup. Somehow with type annotation changes we "broke" some tests # assert err == "" assert ret == 0 # but we must not fail overall regardless if ( os.environ.get("DUECREDIT_ENABLE", False) and on_windows ): # TODO this test fails on windows pytest.xfail("Fails for some reason on Windows") elif os.environ.get("DUECREDIT_ENABLE", False): # we enabled duecredit if ( os.environ.get("DUECREDIT_REPORT_TAGS", None) == "*" and kwargs.get("script") ) or "numpy" in kwargs.get("cmd", ""): # we requested to have all tags output, and used bibtex in our entry # Somewhere (in out or err) we announce a problem assert "For formatted output we need citeproc" in out + err else: # there was nothing to format so we did not fail for no reason assert "For formatted output we need citeproc" not in out assert "0 packages cited" in out assert "done123" in out elif os.environ.get("DUECREDIT_TEST_EARLY_IMPORT_ERROR"): assert "ImportError" in out + err assert "DUECREDIT_TEST_EARLY_IMPORT_ERROR" in out + err if direct_duecredit_import: assert "Please report" in out + err else: assert "done123" in out else: assert "done123\n" or "done123\r\n" == out if __name__ == "__main__": from duecredit import due test_api(due) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_cmdline.py0000644000175100001770000000315714627677134021334 0ustar00runnerdocker# 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 __future__ import annotations from io import StringIO import sys import pytest from pytest import MonkeyPatch from .. import __version__ from ..cmdline import main def test_import() -> None: import duecredit.cmdline # noqa: F401 import duecredit.cmdline.main # noqa: F401 def test_main_help(monkeypatch: MonkeyPatch) -> None: # Patch stdout fakestdout = StringIO() monkeypatch.setattr(sys, "stdout", fakestdout) pytest.raises(SystemExit, main.main, ["--help"]) assert fakestdout.getvalue().lstrip().startswith("Usage: ") def test_main_version(monkeypatch: MonkeyPatch) -> None: # Patch stdout or stderr for different Python versions -- catching both fakestdout = StringIO() fakeout = "stdout" monkeypatch.setattr(sys, fakeout, fakestdout) pytest.raises(SystemExit, main.main, ["--version"]) assert (fakestdout.getvalue()).split("\n")[0] == "duecredit %s" % __version__ # smoke test the cmd_summary # TODO: carry sample .duecredit.p, point to that file, monkeypatch TextOutput and BibTeXOutput .dumps def test_smoke_cmd_summary() -> None: main.main(["summary"]) def test_cmd_test() -> None: # test the not implemented cmd_test pytest.raises(SystemExit, main.main, ["test"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_collector.py0000644000175100001770000002567014627677134021713 0ustar00runnerdocker# 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 __future__ import annotations import os from typing import TYPE_CHECKING, Any import pytest from ..collector import Citation, CitationKey, CollectorSummary, DueCreditCollector from ..dueswitch import DueSwitch from ..entries import BibTeX, Doi from ..io import PickleOutput from ..stub import InactiveDueCreditCollector if TYPE_CHECKING: import py def _test_entry(due, entry) -> None: 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 = {https://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() -> None: entry = BibTeX(_sample_bibtex) cit1 = Citation(entry, path="somemodule") assert cit1.cites_module assert cit1.module == "somemodule" cit2 = Citation(entry, path="somemodule.submodule") assert cit2.cites_module assert cit2.module == "somemodule.submodule" assert cit1 in cit1 assert cit2 in cit1 assert cit1 not in cit2 cit3 = Citation(entry, path="somemodule.submodule:class2.func2") assert not cit3.cites_module assert cit3.module == "somemodule.submodule" assert cit2 in cit1 assert cit3 in cit1 assert cit3 in cit2 assert cit2 not in cit3 cit4 = Citation(entry, path="somemodule2:class2.func2") assert not cit4.cites_module assert cit4.module == "somemodule2" assert cit1 not in cit4 assert cit4 not in cit1 def test_entry() -> None: entry = BibTeX(_sample_bibtex) _test_entry(DueCreditCollector(), entry) entries = [BibTeX(_sample_bibtex), BibTeX(_sample_bibtex), Doi(_sample_doi)] _test_entry(DueCreditCollector(), entries) def _test_dcite_basic(_due, func) -> None: assert func("magical", 1) == "load" # verify that @wraps correctly passes all the docstrings etc assert func.__name__ == "method" assert func.__doc__ == "docstring" def test_dcite_method() -> None: # Test basic wrapping that we don't mask out the arguments for due in [DueCreditCollector(), InactiveDueCreditCollector()]: assert isinstance(due, (DueCreditCollector, InactiveDueCreditCollector)) active = isinstance(due, DueCreditCollector) due.add(BibTeX(_sample_bibtex)) @due.dcite("XXX0", path="method") def method(arg1: str, kwarg2: str = "blah") -> str: """docstring""" assert arg1 == "magical" assert kwarg2 == 1 return "load" class SomeClass: @due.dcite("XXX0", path="someclass:method") # type: ignore def method(self, arg1: str, kwarg2: str = "blah") -> str: """docstring""" assert arg1 == "magical" assert kwarg2 == 1 return "load" if active: assert isinstance(due, DueCreditCollector) assert due.citations == {} assert len(due._entries) == 1 _test_dcite_basic(due, method) if active: assert isinstance(due, DueCreditCollector) assert len(due.citations) == 1 assert len(due._entries) == 1 citation = due.citations[CitationKey("method", "XXX0")] assert citation.count == 1 # TODO: this is probably incomplete path but unlikely we would know # any better assert citation.path == "method" instance = SomeClass() _test_dcite_basic(due, instance.method) if active: assert isinstance(due, DueCreditCollector) assert len(due.citations) == 2 assert len(due._entries) == 1 # TODO: we should actually get path/counts pairs so here citation = due.citations[CitationKey("someclass:method", "XXX0")] assert citation.path == "someclass:method" assert citation.count == 1 # And we explicitly stated that module need to be cited assert citation.cite_module class SomeClass2: # Used to test for classes that are not instantiated @due.dcite("XXX0", path="some.module.without.method") # type: ignore def method2(self, arg1, kwarg2="blah"): # noqa: U100 assert arg1 == "magical" return "load" # and a method pointing to the module instance2 = SomeClass() _test_dcite_basic(due, instance2.method) if active: assert isinstance(due, DueCreditCollector) assert len(due.citations) == 2 # different paths assert 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 citation.cite_module def _test_args_match_conditions(conds: dict[tuple[int, str], set[str]]) -> None: args_match_conditions = DueCreditCollector._args_match_conditions assert args_match_conditions(conds) assert args_match_conditions(conds, None) assert args_match_conditions(conds, someirrelevant=True) assert args_match_conditions(conds, method="purge") assert args_match_conditions(conds, method="fullpurge") assert args_match_conditions(conds, None, "purge") assert args_match_conditions(conds, None, "fullpurge") assert args_match_conditions(conds, None, "fullpurge", someirrelevant="buga") assert not args_match_conditions(conds, None, "push") assert not args_match_conditions(conds, method="push") if len(conds) < 2: return # got compound case assert args_match_conditions(conds, scope="life") assert not args_match_conditions(conds, scope="someother") # should be "and", so if one not matching -- both not matching assert not args_match_conditions(conds, method="wrong", scope="life") assert not args_match_conditions(conds, method="purge", scope="someother") # assert args_match_conditions(conds, None, None, 'life') # ambiguous/conflicting def test_args_match_conditions() -> None: _test_args_match_conditions({(1, "method"): {"purge", "fullpurge", "DC_DEFAULT"}}) _test_args_match_conditions( { (1, "method"): {"purge", "fullpurge", "DC_DEFAULT"}, (2, "scope"): {"life", "DC_DEFAULT"}, } ) def _test_dcite_match_conditions(due, func, path: str) -> None: assert due.citations == {} assert len(due._entries) == 1 assert func("magical", "unknown") == "load unknown" assert due.citations == {} assert len(due._entries) == 1 assert func("magical") == "load blah" assert len(due.citations) == 1 assert len(due._entries) == 1 entry = due._entries["XXX0"] # noqa: F841 assert due.citations[(path, "XXX0")].count == 1 # Cause the same citation assert func("magical", "blah") == "load blah" # Nothing should change assert len(due.citations) == 1 assert len(due._entries) == 1 assert due.citations[(path, "XXX0")].count == 2 # Besides the count # Now cause new citation given another value assert func("magical", "boo") == "load boo" assert len(due.citations) == 2 assert len(due._entries) == 2 assert ( due.citations[(path, "XXX0")].count == 2 ) # Count should stay the same for XXX0 assert ( due.citations[(path, "10.3389/fninf.2012.00022")].count == 1 ) # but we get a new one def test_dcite_match_conditions_function() -> None: 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: str, kwarg2: str = "blah") -> str: """docstring""" assert arg1 == "magical" return "load %s" % kwarg2 _test_dcite_match_conditions(due, method, "callable") def test_dcite_match_conditions_method() -> None: due = DueCreditCollector() due.add(BibTeX(_sample_bibtex)) class Citeable: def __init__(self, param: str | None = None) -> 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: str, kwarg2: str = "blah") -> str: """docstring""" assert arg1 == "magical" return "load %s" % kwarg2 citeable = Citeable(param="paramvalue") _test_dcite_match_conditions(due, citeable.method, "obj.callable") # now test for self.param - def test_get_output_handler_method(tmpdir: py.path.local, monkeypatch) -> None: tempfile = str(tmpdir.mkdir("sub").join("tempfile.txt")) monkeypatch.setitem(os.environ, "DUECREDIT_OUTPUTS", "pickle") entry = BibTeX(_sample_bibtex) collector = DueCreditCollector() collector.cite(entry, path="module") summary = CollectorSummary(collector, fn=tempfile) handlers = [summary._get_output_handler(type_, collector) for type_ in ["pickle"]] # assert isinstance(handlers[0], TextOutput) assert isinstance(handlers[0], PickleOutput) pytest.raises( NotImplementedError, summary._get_output_handler, "nothing", collector ) def test_collectors_uniform_api() -> None: def get_api(objs) -> list[str]: return [ x for x in sorted(sum((dir(obj) for obj in objs), [])) if not x.startswith("_") or x in "__call__" ] assert get_api([DueCreditCollector, DueSwitch]) == get_api( [InactiveDueCreditCollector] ) def _test__docs__(method: Any) -> None: assert "entry:" in method.__doc__ assert "tags: " in method.__doc__ def test__docs__() -> None: _test__docs__(DueCreditCollector.cite) _test__docs__(DueCreditCollector.dcite) _test__docs__(Citation.__init__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_dueswitch.py0000644000175100001770000000343514627677134021717 0ustar00runnerdocker# 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 __future__ import annotations import atexit from typing import Any import pytest from pytest import MonkeyPatch from ..dueswitch import DueSwitch, due from ..injections.injector import DueCreditInjector def test_dueswitch_activate(monkeypatch: MonkeyPatch) -> None: if due.active: pytest.skip("due is already active, can't test more at this point") # TODO: use TypedDict PEP 692 or a small typed object state = dict(activate=0, register=0, register_func=None) # Patch DueCreditInjector.activate def activate_calls(*_args: Any, **_kwargs: Any) -> None: assert type(state["activate"]) is int state["activate"] += 1 monkeypatch.setattr(DueCreditInjector, "activate", activate_calls) # Patch atexit.register def register(func) -> None: assert type(state["register"]) is int state["register"] += 1 state["register_func"] = func monkeypatch.setattr(atexit, "register", register) due.activate() # was not active, so should have called activate of the injector class assert state["activate"] == 1 assert state["register"] == 1 assert state["register_func"] == due.dump def test_a_bad_one() -> None: # We might get neither of those and should fail # see https://github.com/duecredit/duecredit/issues/142 # So let's through ValueError right away pytest.raises(ValueError, DueSwitch, None, None, True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_entries.py0000644000175100001770000000153114627677134021364 0ustar00runnerdocker# 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 ..entries import Doi, Text, Url def test_comparison() -> None: assert Text("123") == Text("123") assert Text("123") != Text("124") assert Text("123", "key") == Text("123", "key") assert Text("123", "key") != Text("123", "key1") assert Text("123", "key") != Text("124", "key") assert Doi("123/1", "key") == Doi("123/1", "key") assert Url("http://123/1", "key") == Url("http://123/1", "key") def test_sugaring_api() -> None: assert Url("http://1.com").url == "http://1.com" assert Doi("1.com").doi == "1.com" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_import_doi0000644000175100001770000003257514627677134021445 0ustar00runnerdockerinteractions: - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.18.4] method: GET uri: https://doi.org/10.1038/nrd842 response: body: {string: !!python/unicode 'Handle Redirect https://data.crossref.org/10.1038%2Fnrd842'} headers: cf-ray: [438fadfe5a8b5a68-BOS] connection: [keep-alive] content-length: ['169'] content-type: [text/html;charset=utf-8] date: ['Thu, 12 Jul 2018 01:19:06 GMT'] expect-ct: ['max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"'] expires: ['Thu, 12 Jul 2018 01:37:12 GMT'] location: ['https://data.crossref.org/10.1038%2Fnrd842'] server: [cloudflare] set-cookie: ['__cfduid=dcbb38a7253a023869b367a39d8a2b2ae1531358345; expires=Fri, 12-Jul-19 01:19:05 GMT; path=/; domain=.doi.org; HttpOnly'] vary: [Accept] status: {code: 302, message: ''} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.18.4] method: GET uri: https://data.crossref.org/10.1038%2Fnrd842 response: body: {string: !!python/unicode "@article{Atkins_2002,\n\tdoi = {10.1038/nrd842},\n\turl = {https://doi.org/10.1038%2Fnrd842},\n\tyear = 2002,\n\tmonth = {jul},\n\tpublisher = {Springer Nature},\n\tvolume = {1},\n\tnumber = {7},\n\tpages = {491--492},\n\tauthor = {Joshua H. Atkins and Leland J. Gershell},\n\ttitle = {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] connection: [close] content-length: ['339'] content-type: [application/x-bibtex] date: ['Thu, 12 Jul 2018 01:19:06 GMT'] link: ['; rel="canonical", ; version="vor"; type="application/pdf"; rel="item", ; version="vor"; type="text/html"; rel="item", ; version="vor"; type="application/pdf"; rel="item", ; rel="license"'] server: [http-kit] x-rate-limit-interval: [1s] x-rate-limit-limit: ['50'] 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.18.4] method: GET uri: https://doi.org/fasljfdldaksj response: body: string: !!binary | H4sIAAAAAAAAA6xYW3PbuBV+1684i53uUyjqlrQrk5yRbXnjqW25stw0TxkIOBSxIQEuAEpWZ3/8 DkDSuthx7LR6ES8493M+fGD00/nsbPH5dgqZLXK4vT+9ujwDEoThp+FZGJ4vzuE/HxfXV9Dv9mCh qTTCCiVpHobTG9KB5kcya8txGG42m+5m2FV6FS7m4YPT2XdKmsvA7mnocstJ0oncG/eHlCedyAqb YzLVWukxnM8u4UZZuFCV5FFYv+t0ogItBWcywD8qsY7JmZIWpQ0W2xIJsPouJhYfbOj0nwDLqDZo 4/vFRfAPAqFTkwv5FTTmMRFMSQKZxjQmobHUChaKYhWmdO1edUu58jJ7IiZT2rLKwnOyrZxgioDd lhgTUdAVhg9BvTxMoFF2KGjsNsdQ4ibwV4MuM4Y0Bt0DkyHaVqUPzy9wroV1BjvRUvFt0ul0Ii7W 4KVisqTs60q7NI5/TtlyNPjgUi+KFRjNDmNeUilRB6P+sLsSKQGa25hcqZUisBHcZjH5MOgRyFCs MhuT/vDvBJZKc9Qx6TWucLFODh2ol4/75cPJvi89/yNJI/IKifej94NR+haJ9MOv/T4+SnQ60U9B AIvJ6dUULmZzuJn8+/K3yeJydgOnkzkEgWtDusyxjbff6/3tIEaGeV5SzoVcPd6bkrL2XvCYSLr2 SlwCxUrGhKG0qF3arU785ESWtyaGo11GBx8ILFdM5UrH5OfBsJ8OXIaeLZYfqJJqlHavWo+V6u/V yZUmCi2vbT868KJVYDk1pg4GHyxp/KZN1+5NPVfCj72QHB+6buZI8nF2PY1CmsAvcmnKkz/rv5ek s2UrOrk5P53N/vk28ZQy6yfENGouJmeLu4/T6eLurYr+eNTwL/NU9sUUaDSq0gxbJ+bTu9n9/Gz6 Rh8qg7pVcX83nb9RnEqpKsmw0XAz/fRGBYKnQYHFErU5qOr19Pp0Or+DyXw6cRrrlnCN1fRVFLoG j0Lf/kk9bdOb85cn7jWDjEM6GvGXRn94JDHio19Hg73RP579s9nNYnqzcC7UO9n/Ovq7EUr9r513 N2pMuaUyJt8G3+/Nc28Ped18HgOvG+86/7VZb3fdIJBVJXGPskFytLNmg7YCzcBzsRbcoVXTJ7v8 ZcOk3wtTavLfU55z+tX8HoXZ8LXynahMFpkwfm9nrkktLBFS5wcICTZD/+puaywWXYBbZYxwFdFI jZIGqMZxFJYeyKu83sfbNmgqEyyVtaoYQ/c9FickWTRKhQEhmdIamXXGtqrSUA9rF+6QapZBqrR3 QlgsYLkFSQt8B556vAOlQdkMNTgGwqmlUBkhV0DB1NIoV0JiNwpz8SbPNtQAU6VAvvMw33bhLEP2 FawCgwg2o9b7Zqx2VoVkecXRAM1z/9yRHMosagNLTJVGoJIDTS3WMZmcmsw/k06jY0kMoawksxV1 rAwKqr+at7ufUQN1JVECZVasqUUOW7SugjlSg2D1FuiKCgk5tajfeT80lkrXQZVaLXMsQKT+Fh0F 9FROyAp3PoW+5q/qtqhMPqsKCrrd2RGm0WyVN6PRlErWHeYimeNKGKvrbExWKNm2qbFbnSpdwBJz tekCXNbpr7sICypyoJxrNMYp18hQrNFFkApd1ApdyCkidxjVrZt4H8XaLOeY2jGMfIo7nchbdVlV MiahVNZPC3FNmCkek1IZSwAlq2khLctcMG8wfAg2m03gFASVzlEyxZET39Qxkcr6+b9QuiB+nmrs O4K7R3gbHsHf0INbTTdbkEue03LIgI6VHBt5VNrozKBVoB3wETBMuTi12pAkyukSc4dn4yisr6PQ ZrUvkZBlZZtoC2FcHT9SyR0r21Fo4gCyQoeuh7hGwIj/YkxGA0fBKVcy38akvdrxqWbDa739rrP3 8ytQKXzCJdzSFcKVMLbtsFcEojFF7QDACT8byP/d8c+uxae+xSd1i4+/4ZxjLX5hs+5Z/xpE8O3w zPjsu/9WVw83uwnnzWkTLqWbgmaul6qqQac+aO7F4vykGmkTDlNFgdIa4jfvmAx7BLTaGL+HR2G7 +tDJsJ2JlgJ1nsTg9+WDIJKoPMykQTfjdfZMtSzELn93/rZ2HuYe2upMld93xGVhd0TrPPrmC/kD lGTwEiM58qZ14il0B2mV50/w+5CrXcxmi2nLFmuMeQszOz6G7XjZMTy1NG3goKhlyGGGedlQ4B1B gVutHrZwh3qNGs4Vq1y7+C7z3PgpI9sz6GB+l7xR75us0J2+vyyu688QPv3ns8svXBj2Jd8/mA/2 T5GjPUr4xGrSiVyMz288vfLhBFIlbeDmcAx9R8RJciCxRm0Fo3ngtY7BVCXqE5L8onF1EoVuafLO odmPSXVn899+QNLtr/7DzI8YdswSrKYcPQtyEO0g4tJ1haQNjLjS+03TP+g2op3H848/QhwdgMJ2 CutPXX8BAAD//wMABwukynQTAAA= headers: cf-ray: [438fae01aa93ae20-BOS] connection: [keep-alive] content-encoding: [gzip] content-type: [text/html;charset=utf-8] date: ['Thu, 12 Jul 2018 01:19:07 GMT'] expect-ct: ['max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"'] server: [cloudflare] set-cookie: ['__cfduid=d41616872f8f3089311dd1db1ed02c6a11531358346; expires=Fri, 12-Jul-19 01:19:06 GMT; path=/; domain=.doi.org; HttpOnly'] status: {code: 404, message: ''} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.18.4] method: GET uri: https://doi.org/fasljfdldaksj response: body: string: !!binary | H4sIAAAAAAAAA6xYW3PbuBV+1684i53uUyjqlrQrk5yRbXnjqW25stw0TxkIOBSxIQEuAEpWZ3/8 DkDSuthx7LR6ES8493M+fGD00/nsbPH5dgqZLXK4vT+9ujwDEoThp+FZGJ4vzuE/HxfXV9Dv9mCh qTTCCiVpHobTG9KB5kcya8txGG42m+5m2FV6FS7m4YPT2XdKmsvA7mnocstJ0oncG/eHlCedyAqb YzLVWukxnM8u4UZZuFCV5FFYv+t0ogItBWcywD8qsY7JmZIWpQ0W2xIJsPouJhYfbOj0nwDLqDZo 4/vFRfAPAqFTkwv5FTTmMRFMSQKZxjQmobHUChaKYhWmdO1edUu58jJ7IiZT2rLKwnOyrZxgioDd lhgTUdAVhg9BvTxMoFF2KGjsNsdQ4ibwV4MuM4Y0Bt0DkyHaVqUPzy9wroV1BjvRUvFt0ul0Ii7W 4KVisqTs60q7NI5/TtlyNPjgUi+KFRjNDmNeUilRB6P+sLsSKQGa25hcqZUisBHcZjH5MOgRyFCs MhuT/vDvBJZKc9Qx6TWucLFODh2ol4/75cPJvi89/yNJI/IKifej94NR+haJ9MOv/T4+SnQ60U9B AIvJ6dUULmZzuJn8+/K3yeJydgOnkzkEgWtDusyxjbff6/3tIEaGeV5SzoVcPd6bkrL2XvCYSLr2 SlwCxUrGhKG0qF3arU785ESWtyaGo11GBx8ILFdM5UrH5OfBsJ8OXIaeLZYfqJJqlHavWo+V6u/V yZUmCi2vbT868KJVYDk1pg4GHyxp/KZN1+5NPVfCj72QHB+6buZI8nF2PY1CmsAvcmnKkz/rv5ek s2UrOrk5P53N/vk28ZQy6yfENGouJmeLu4/T6eLurYr+eNTwL/NU9sUUaDSq0gxbJ+bTu9n9/Gz6 Rh8qg7pVcX83nb9RnEqpKsmw0XAz/fRGBYKnQYHFErU5qOr19Pp0Or+DyXw6cRrrlnCN1fRVFLoG j0Lf/kk9bdOb85cn7jWDjEM6GvGXRn94JDHio19Hg73RP579s9nNYnqzcC7UO9n/Ovq7EUr9r513 N2pMuaUyJt8G3+/Nc28Ped18HgOvG+86/7VZb3fdIJBVJXGPskFytLNmg7YCzcBzsRbcoVXTJ7v8 ZcOk3wtTavLfU55z+tX8HoXZ8LXynahMFpkwfm9nrkktLBFS5wcICTZD/+puaywWXYBbZYxwFdFI jZIGqMZxFJYeyKu83sfbNmgqEyyVtaoYQ/c9FickWTRKhQEhmdIamXXGtqrSUA9rF+6QapZBqrR3 QlgsYLkFSQt8B556vAOlQdkMNTgGwqmlUBkhV0DB1NIoV0JiNwpz8SbPNtQAU6VAvvMw33bhLEP2 FawCgwg2o9b7Zqx2VoVkecXRAM1z/9yRHMosagNLTJVGoJIDTS3WMZmcmsw/k06jY0kMoawksxV1 rAwKqr+at7ufUQN1JVECZVasqUUOW7SugjlSg2D1FuiKCgk5tajfeT80lkrXQZVaLXMsQKT+Fh0F 9FROyAp3PoW+5q/qtqhMPqsKCrrd2RGm0WyVN6PRlErWHeYimeNKGKvrbExWKNm2qbFbnSpdwBJz tekCXNbpr7sICypyoJxrNMYp18hQrNFFkApd1ApdyCkidxjVrZt4H8XaLOeY2jGMfIo7nchbdVlV MiahVNZPC3FNmCkek1IZSwAlq2khLctcMG8wfAg2m03gFASVzlEyxZET39Qxkcr6+b9QuiB+nmrs O4K7R3gbHsHf0INbTTdbkEue03LIgI6VHBt5VNrozKBVoB3wETBMuTi12pAkyukSc4dn4yisr6PQ ZrUvkZBlZZtoC2FcHT9SyR0r21Fo4gCyQoeuh7hGwIj/YkxGA0fBKVcy38akvdrxqWbDa739rrP3 8ytQKXzCJdzSFcKVMLbtsFcEojFF7QDACT8byP/d8c+uxae+xSd1i4+/4ZxjLX5hs+5Z/xpE8O3w zPjsu/9WVw83uwnnzWkTLqWbgmaul6qqQac+aO7F4vykGmkTDlNFgdIa4jfvmAx7BLTaGL+HR2G7 +tDJsJ2JlgJ1nsTg9+WDIJKoPMykQTfjdfZMtSzELn93/rZ2HuYe2upMld93xGVhd0TrPPrmC/kD lGTwEiM58qZ14il0B2mV50/w+5CrXcxmi2nLFmuMeQszOz6G7XjZMTy1NG3goKhlyGGGedlQ4B1B gVutHrZwh3qNGs4Vq1y7+C7z3PgpI9sz6GB+l7xR75us0J2+vyyu688QPv3ns8svXBj2Jd8/mA/2 T5GjPUr4xGrSiVyMz288vfLhBFIlbeDmcAx9R8RJciCxRm0Fo3ngtY7BVCXqE5L8onF1EoVuafLO odmPSXVn899+QNLtr/7DzI8YdswSrKYcPQtyEO0g4tJ1haQNjLjS+03TP+g2op3H848/QhwdgMJ2 CutPXX8BAAD//wMABwukynQTAAA= headers: cf-ray: [438fae063d57ae38-BOS] connection: [keep-alive] content-encoding: [gzip] content-type: [text/html;charset=utf-8] date: ['Thu, 12 Jul 2018 01:19:07 GMT'] expect-ct: ['max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"'] server: [cloudflare] set-cookie: ['__cfduid=d65b995bd178e3cd707285c55f233c7621531358347; expires=Fri, 12-Jul-19 01:19:07 GMT; path=/; domain=.doi.org; HttpOnly'] status: {code: 404, message: ''} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.18.4] method: GET uri: https://doi.org/10.5281/zenodo.50186 response: body: {string: !!python/unicode 'Handle Redirect https://data.datacite.org/10.5281%2Fzenodo.50186'} headers: cf-ray: [438fae0948585a68-BOS] connection: [keep-alive] content-length: ['181'] content-type: [text/html;charset=utf-8] date: ['Thu, 12 Jul 2018 01:19:07 GMT'] expect-ct: ['max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"'] expires: ['Thu, 12 Jul 2018 01:49:59 GMT'] location: ['https://data.datacite.org/10.5281%2Fzenodo.50186'] server: [cloudflare] set-cookie: ['__cfduid=d98c43f90ddddadde258529e39bcd39b91531358347; expires=Fri, 12-Jul-19 01:19:07 GMT; path=/; domain=.doi.org; HttpOnly'] vary: [Accept] status: {code: 302, message: ''} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.18.4] method: GET uri: https://data.datacite.org/10.5281%2Fzenodo.50186 response: body: string: !!binary | H4sIAI6sRlsAA2xVwW7bOBC95yv4AYpsB2hRFChQJ3GU2I5jxMV2N7exNJYmokiFpOzYRb6mp/7A nvaWH9shJWXj3b0E1gw58+a9x8nXimz6o3Cutp8Hg0xTrE0+GA3jD2efRoMDKp3p+MNw9OljdCIE 58UX8eP/0i8+3xjp8325Lu8rGky1yQb/HIXGFdr40wnntcSdLSkSM3PY24PTGzGNBahMTKzDNahI 3NkUTAidN0bZSFwUhqzTdYFt+IEwl2giMTFUhsiSlAWTReIcLEkMsVvIyGqud0Eqwzb2HWypq0jc UloAyravlMRNLmFLmUggb09eSDBliIbPS9h7aO/v3YIqXn+iPV3xTR2JscRnjpv2/kI75yH2N5YF SarrkJvqQgVk16BsWx53uGf0EkoUkxC6BpkWqEqu/AcYbSUjuYu7VAUSSKEfDozUITpDW8DWoxwr 4p/HYyjqYF83yqFaNyaPxLSRBOK2r6pKPB5xwb+1ZHLOYx5IgupJtGgqUNzqEivqwhPumRes7Njn OopMSbij9PBfEX/joZ4aaJ4jJr3rN065h1cR1SNU1Ba50maNjGGK1lLajnWvS6x8p36qmVY8z++M JT99YNpCMDFQbbRx/9ZmJhEtYelrdgKs/NAbL1h3tEO5orKESKwKMJC1VkODh9OkwS2HIj+iY6BX yOq21RMJLnjxmx+4Y+LmaQ2yrUNPITKnhr/piVpOkdXuTRga88k6EktoZEe6UR7eVNuiaUmYgXWV 1/vtGcyhqlnCOWqWu0dbkn9Dc1YQXNHBueBpMp0yA/cxW6hCo9vE7PUv50Kje1Svv9pXyCQTbp3V W0ZY7Z1pDZcg2fAM37krQVOE4VcO677bnLxYVwbUgWzZ2C7IdjM2e/3TBN6noLRNi943OVsT7Vvt VevRscrW6J27YnvwCmGCe/utHGQBzZQr5l0IUeKx9fzba6cymkUmz8F34hUAVcuM1rWvkrASnf+k NhheFftn170h8Hab02NHc6LVASSyzW+2vffTBfOKR+wsoYZM17qR2oo7s/Ej8wGqyBnq9JIyaPqG k11US3TuGGZSsA88Dc7AI7mwaEvc73j1Wr9qFTZGUwU5eaAcLjdS7yJRs0slb45wwZHjXcmnF1Tv a/wsxjwsPtPakzanvHA79H/96GLyzGvD+pxYvCvO4zkQS6NT/zz5m2Wu0DcUN0os97z7VSyG8egs Hp7ep6PQuG7WvHO9HNz8IfzvCPE9QgidDUcfX05eTv4GAAD//wMA46P4VLQGAAA= headers: accept: [application/x-bibtex] cache-control: [private] connection: [keep-alive] content-disposition: [attachment; filename=_10.5281_zenodo.50186.bib] content-encoding: [gzip] content-transfer-encoding: [binary] content-type: [application/x-bibtex] date: ['Thu, 12 Jul 2018 01:19:10 GMT'] etag: [W/"a60a721479c694d11c63cfa61bc95c18"] server: [nginx/1.14.0 + Phusion Passenger 5.3.3] status: [200 OK] vary: ['Accept-Encoding, Origin'] x-powered-by: [Phusion Passenger 5.3.3] x-request-id: [83620554-e6e4-4b99-82ba-e8d9c255bdab] x-runtime: ['1.929840'] status: {code: 200, message: OK} version: 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_injections.py0000644000175100001770000003141214627677134022061 0ustar00runnerdocker# 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 __future__ import annotations import builtins as __builtin__ import gc from logging import getLogger import sys from typing import Any import pytest from duecredit.collector import DueCreditCollector from duecredit.entries import Doi import duecredit.tests.mod as mod from .. import __version__ from ..injections.injector import ( DueCreditInjector, find_object, get_modules_for_injection, ) from ..versions import Version try: import mvpa2 # noqa: F401 _have_mvpa2 = True except ImportError: _have_mvpa2 = False _orig__import__ = __builtin__.__import__ lgr = getLogger("duecredit.tests.injector") class TestActiveInjector: def setup_method(self) -> None: 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_method(self) -> None: 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 __builtin__.__import__ is _orig__import__ self._cleanup_modules() def _cleanup_modules(self) -> None: if "duecredit.tests.mod" in sys.modules: sys.modules.pop("duecredit.tests.mod") def _test_simple_injection(self, func, import_stmt, func_call=None) -> None: assert "duecredit.tests.mod" not 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 "duecredit.tests.mod" not in sys.modules # no import happening assert len(self.due._entries) == 0 assert len(self.due.citations) == 0 globals_: dict[str, Any] = {} locals_: dict[str, Any] = {} exec(import_stmt, globals_, locals_) assert len(self.due._entries) == 1 # we should get an entry now assert len(self.due.citations) == 0 # but not yet a citation import duecredit.tests.mod as mod _, _, obj = find_object(mod, func) assert obj.__duecredited__ # we wrapped assert obj.__duecredited__ is not obj # and it is not pointing to the same func assert 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_) # TODO: awkwardly 'ret' is not found in the scope while running pytest # under python3.4, although present in locals()... WTF? assert locals_["ret"] == "%s: None, somevalue" % func assert len(self.due._entries) == 1 assert len(self.due.citations) == 1 # TODO: there must be a cleaner way to get first value citation = list(self.due.citations.values())[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 citation.version == Version(__version__) assert citation.tags == ["implementation", "very custom"] def _test_double_injection(self, func, import_stmt, func_call=None) -> None: assert "duecredit.tests.mod" not 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 "duecredit.tests.mod" not in sys.modules # no import happening assert len(self.due._entries) == 0 assert len(self.due.citations) == 0 globals_: dict[str, Any] = {} locals_: dict[str, Any] = {} exec(import_stmt, globals_, locals_) assert len(self.due._entries) == 2 # we should get two entries now assert len(self.due.citations) == 0 # but not yet a citation import duecredit.tests.mod as mod _, _, obj = find_object(mod, func) assert obj.__duecredited__ # we wrapped assert obj.__duecredited__ is not obj # and it is not pointing to the same func assert 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_) # TODO: awkwardly 'ret' is not found in the scope while running pytest # under python3.4, although present in locals()... WTF? assert locals_["ret"] == "%s: None, somevalue" % func assert len(self.due._entries) == 2 assert len(self.due.citations) == 2 # TODO: there must be a cleaner way to get first value citation = list(self.due.citations.values())[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 citation.version, __version__ assert citation.tags == ["implementation", "very custom"] test1 = ("testfunc1", "from duecredit.tests.mod import testfunc1", None) test2 = ( "TestClass1.testmeth1", "from duecredit.tests.mod import TestClass1; c = TestClass1()", "c.testmeth1", ) test3 = ( "TestClass12.Embed.testmeth1", "from duecredit.tests.mod import TestClass12; c = TestClass12.Embed()", "c.testmeth1", ) @pytest.mark.parametrize("func, import_stmt, func_call", [test1, test2, test3]) def test_simple_injection(self, func, import_stmt, func_call) -> None: self._test_simple_injection(func, import_stmt, func_call) @pytest.mark.parametrize("func, import_stmt, func_call", [test1, test2, test3]) def test_double_injection(self, func, import_stmt, func_call) -> None: self._test_double_injection(func, import_stmt, func_call) def test_delayed_entries(self) -> None: # verify that addition of delayed injections happened modules_for_injection = get_modules_for_injection() assert len(self.injector._delayed_injections) == len(modules_for_injection) assert 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 # noqa: F401 except ImportError as e: pytest.skip("scipy was not found: {}".format(e)) def test_import_mvpa2_suite(self) -> None: if not _have_mvpa2: pytest.skip("no mvpa2 found") # just a smoke test for now import mvpa2.suite as mv # noqa: F401 def _test_incorrect_path(self, mod, obj) -> None: 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", {}, {}) @pytest.mark.parametrize( "mod, obj", [ ("nonexistingmodule", None), ("duecredit.tests.mod.nonexistingmodule", None), ("duecredit.tests.mod", "nonexisting"), ("duecredit.tests.mod", "nonexisting.whocares"), ], ) def test_incorrect_path(self, mod, obj): self._test_incorrect_path(mod, obj) def test_find_iobject() -> None: assert find_object(mod, "testfunc1") == (mod, "testfunc1", mod.testfunc1) assert find_object(mod, "TestClass1") == (mod, "TestClass1", mod.TestClass1) assert find_object(mod, "TestClass1.testmeth1") == ( mod.TestClass1, "testmeth1", mod.TestClass1.testmeth1, ) assert find_object(mod, "TestClass12.Embed.testmeth1") == ( mod.TestClass12.Embed, "testmeth1", mod.TestClass12.Embed.testmeth1, ) def test_no_double_activation() -> None: orig__import__ = __builtin__.__import__ try: due = DueCreditCollector() injector = DueCreditInjector(collector=due) injector.activate() assert __builtin__.__import__ is not orig__import__ duecredited__import__ = __builtin__.__import__ # TODO: catch/analyze/swallow warning injector.activate() assert ( __builtin__.__import__ is duecredited__import__ ) # we didn't decorate again finally: injector.deactivate() __builtin__.__import__ = orig__import__ def test_get_modules_for_injection() -> None: # output order is sorted by name (not that it matters for functionality) assert get_modules_for_injection() == [ "mod_biosig", "mod_dipy", "mod_matplotlib", "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() -> None: # 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() -> None: # 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() -> None: orig__import__ = __builtin__.__import__ try: due = DueCreditCollector() inj = DueCreditInjector(collector=due) del inj # delete inactive assert __builtin__.__import__ is orig__import__ inj = DueCreditInjector(collector=due) inj.activate(retrospect=False) assert __builtin__.__import__ is not orig__import__ assert inj._orig_import is not None del inj # delete active but not used inj = None # type: ignore # noqa: F841 __builtin__.__import__ = None # type: ignore # /\ We need to do that since otherwise gc will not pick up inj gc.collect() # To cause __del__ assert __builtin__.__import__ is orig__import__ import abc # noqa: F401 # and new imports work just fine finally: __builtin__.__import__ = orig__import__ def test_injector_delayed_del() -> None: # 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 __builtin__.__import__ is not orig__import__ assert inj._orig_import is not None inj.deactivate() assert __builtin__.__import__ is orig__import__ assert inj._orig_import is None # create 2nd one inj2 = DueCreditInjector(collector=due) inj2.activate(retrospect=False) assert __builtin__.__import__ is not orig__import__ assert inj2._orig_import is not None del inj # type: ignore inj = None # type: ignore # noqa: F841 gc.collect() # To cause __del__ assert ( __builtin__.__import__ is not orig__import__ ) # would fail if del had side-effect assert inj2._orig_import is not None inj2.deactivate() assert __builtin__.__import__ is orig__import__ import abc # noqa: F401 # and new imports work just fine finally: __builtin__.__import__ = orig__import__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_io.py0000644000175100001770000005255114627677134020332 0ustar00runnerdocker# ex: set sts=4 sw=4 et: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from __future__ import annotations from io import StringIO import os import pickle import random import re from typing import Any import pytest from pytest import MonkeyPatch import duecredit.io from .test_collector import _sample_bibtex, _sample_bibtex2, _sample_doi from ..collector import CitationKey, DueCreditCollector from ..entries import BibTeX, Doi, Text, Url from ..io import ( BibTeXOutput, Output, PickleOutput, TextOutput, _is_contained, format_bibtex, get_text_rendering, import_doi, ) try: # TODO: for some reason test below started to complain that we are trying # to overwrite the cassette. # # import vcr # @vcr.use_cassette() def test_import_doi() -> None: doi_good = "10.1038/nrd842" assert isinstance(import_doi(doi_good, sleep=0.00001, retries=2), str) doi_bad = "fasljfdldaksj" with pytest.raises(ValueError): import_doi(doi_bad, sleep=0.00001, retries=2) doi_zenodo = "10.5281/zenodo.50186" assert isinstance(import_doi(doi_zenodo, sleep=0.00001, retries=2), str) except ImportError: # no vcr, and that is in 2015! pass def test_pickleoutput(tmpdir) -> None: # entry = BibTeX('@article{XXX0, ...}') entry = BibTeX( "@article{Atkins_2002,\n" "title=title,\n" "volume=1, \n" "url=https://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()] tempfile = str(tmpdir.mkdir("sub").join("tempfile.txt")) for collector in collectors: pickler = PickleOutput(collector, fn=tempfile) assert pickler.fn == tempfile assert pickler.dump() is None # type: ignore with open(tempfile, "rb") as f: collector_loaded = pickle.load(f) assert collector.citations.keys() == collector_loaded.citations.keys() # TODO: implement comparison of citations assert collector._entries.keys() == collector_loaded._entries.keys() os.unlink(tempfile) def test_output() -> None: 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 len(packages) == 1 assert len(modules) == 1 assert len(objects) == 0 assert ( packages["package"][0] == collector.citations[CitationKey("package", entry.get_key())] ) assert ( modules["package.module"][0] == collector.citations[CitationKey("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 len(packages) == 0 assert len(modules) == 1 assert len(objects) == 0 assert ( modules["package2.module"][0] == collector.citations[CitationKey("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 len(packages) == 1 assert len(modules) == 1 assert len(objects) == 0 assert ( packages["package"][0] == collector.citations[CitationKey("package", entry.get_key())] ) assert ( modules["package2.module"][0] == collector.citations[CitationKey("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 len(packages) == 1 assert len(packages["package"]) == 2 assert len(modules) == 1 assert 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 packs[0] == collector.citations[CitationKey("package", entry2.get_key())] assert packs[1] == collector.citations[CitationKey("package", entry.get_key())] assert ( modules["package.module"][0] == collector.citations[CitationKey("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 len(packages) == 1 assert len(packages["package"]) == 1 assert len(modules) == 1 assert len(objects) == 0 assert ( packages["package"][0] == collector.citations[CitationKey("package", entry.get_key())] ) assert ( modules["package.module"][0] == collector.citations[CitationKey("package.module", entry.get_key())] ) def test_output_return_all(monkeypatch: MonkeyPatch) -> None: 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 not packages assert not modules assert not objects for flag in ["1", "True", "TRUE", "true", "on", "yes"]: monkeypatch.setitem(os.environ, "DUECREDIT_REPORT_ALL", flag) # if _all is None then get the environment packages, modules, objects = output._get_collated_citations(tags=["*"]) assert len(packages) == 2 assert not modules assert not objects # however if _all is set it shouldn't work packages, modules, objects = output._get_collated_citations( tags=["*"], all_=False ) assert not packages assert not modules assert not objects def test_output_tags(monkeypatch: MonkeyPatch) -> None: 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 len(packages) == 1 assert len(modules) == 1 assert not objects packages, modules, objects = output._get_collated_citations() assert not packages assert not modules assert not objects for tags in ["edu", "wip", "edu,wip"]: monkeypatch.setitem(os.environ, "DUECREDIT_REPORT_TAGS", tags) # if tags is None then get the environment packages, modules, objects = output._get_collated_citations() assert len(packages) == (1 if "edu" in tags else 0) assert len(modules) == (1 if "wip" in tags else 0) assert not objects # however if tags is set it shouldn't work packages, modules, objects = output._get_collated_citations( tags=["implementation"] ) assert not packages assert not modules assert not objects def test_text_output() -> None: 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 "0 packages cited" in value, "value was %s" % value assert "0 modules cited" in value, "value was %s" % value assert "0 functions cited" in value, "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 "1 package cited" in value, "value was %s" % value assert "0 modules cited" in value, "value was %s" % value assert "0 functions cited" in value, "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 "1 package cited" in value, "value was %s" % value assert "1 module cited" in value, "value was %s" % value assert "0 functions cited" in value, "value was %s" % value assert "Halchenko, Y.O." in value, "value was %s" % value assert 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 "1 package cited" in value, "value was %s" % value assert "1 module cited" in value, "value was %s" % value assert "0 functions cited" in value, "value was %s" % value assert "Halchenko, Y.O." in value, "value was %s" % value assert "[1, 2]" in value, "value was %s" % value assert "[3]" not in value, "value was %s" % value def test_text_output_dump_formatting() -> None: 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: Any, kwarg2: Any = "blah") -> Any: """docstring""" assert arg1 == "magical" assert 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 "0 modules cited" in value, f"value was {value}" assert "0 functions cited" in value, f"value was {value}" # now we call it -- check it prints stuff strio = StringIO() mymodule("magical", kwarg2=1) TextOutput(strio, due).dump(tags=["*"]) value = strio.getvalue() assert "1 package cited" in value, f"value was {value}" assert "1 function cited" in value, f"value was {value}" assert "(v 0.0.16)" in value, f"value was {value}" assert len(value.split("\n")) == 16, "value was {}".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: Any, kwarg2: Any = "blah") -> Any: """docstring""" assert arg1 == "magical" assert 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: list[str] = [] reference_numbers = [] references = [] for line in lines: match_citation = re.search(r"\[([0-9, ]+)\]$", line) match_reference = re.search(r"^\[([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 set(citation_numbers) == set(reference_numbers) assert len(set(references)) == len(set(citation_numbers)) assert len(citation_numbers) == 8 # verify that we have returned to previous state of filters import warnings assert ("ignore", None, UserWarning, None, 0) not in warnings.filters def test_bibtex_output() -> None: 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 value == "", f"Value was {value}" # impose citing collector = DueCreditCollector() collector.cite(entry, path="package", cite_module=True) strio = StringIO() BibTeXOutput(strio, collector).dump(tags=["*"]) value = strio.getvalue() assert value.strip() == _sample_bibtex.strip(), f"Value was {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 value.strip() == _sample_bibtex.strip(), f"Value was {value}" # no filtering strio = StringIO() BibTeXOutput(strio, collector).dump(tags=["*"]) value = strio.getvalue() assert ( value.strip() == _sample_bibtex.strip() + _sample_bibtex2.rstrip() ), f"Value was {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 value_ == bibtex, f"Value was {value_} instead of {bibtex}" # assert_equal(value_, bibtex, # msg='Value was {0}'.format(value_, bibtex)) def _generate_sample_bibtex() -> str: """ 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 += "{}={{{}}},\n".format(string, value) sample_bibtex += "}" return sample_bibtex def test_get_text_rendering(monkeypatch: MonkeyPatch) -> None: # Patch bibtex_rendering sample_bibtex = BibTeX(_sample_bibtex) def get_bibtex_rendering(*_args: Any, **_kwargs: Any) -> BibTeX: return sample_bibtex monkeypatch.setattr(duecredit.io, "get_bibtex_rendering", get_bibtex_rendering) # Patch format_bibtex fmt_args = {} def format_bibtex(entry: str, style: str) -> None: fmt_args["entry"] = entry fmt_args["style"] = style monkeypatch.setattr(duecredit.io, "format_bibtex", format_bibtex) # test if bibtex type is passed bibtex_output = get_text_rendering(sample_bibtex) assert fmt_args["entry"] == sample_bibtex assert fmt_args["style"] == "harvard1" fmt_args.clear() # test if doi type is passed doi_output = get_text_rendering(Doi(_sample_doi)) assert fmt_args["entry"] == sample_bibtex assert fmt_args["style"] == "harvard1" assert bibtex_output == doi_output def test_text_text_rendering() -> None: text = "I am so free" assert get_text_rendering(Text(text)) == text def test_url_text_rendering() -> None: url = "http://example.com" assert get_text_rendering(Url(url)) == "URL: " + url def test_format_bibtex_zenodo_doi() -> None: """ 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 = {https://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 ( format_bibtex(BibTeX(bibtex_zenodo)) == """Ghosh, S. et al., 2016. nipype: Release candidate 1 for version 0.12.0.""" ) def test_format_bibtex_with_utf_characters() -> None: """ test that we can correctly parse bibtex entry if it contains utf-8 characters """ # this was fetched on 2017-08-16 # replaced Brett with Brรณtt to have utf-8 characters in first author's name as well bibtex_utf8 = ( "@misc{https://doi.org/10.5281/zenodo.60847,\n doi = {10.5281/zenodo.60847},\n url = {" "http://zenodo.org/record/60847},\n author = {Brรณtt, Matthew and Hanke, Michael and Cipollini, " "Ben and {Marc-Alexandre Cรดtรฉ} and Markiewicz, Chris and Gerhard, Stephan and Larson, " "Eric and Lee, Gregory R. and Halchenko, Yaroslav and Kastman, Erik and {Cindeem} and Morency, " "Fรฉlix C. and {Moloney} and Millman, Jarrod and Rokem, Ariel and {Jaeilepp} and Gramfort, " "Alexandre and Bosch, Jasper J.F. Van Den and {Krish Subramaniam} and Nichols, Nolan and {Embaker} " "and {Bpinsard} and {Chaselgrove} and Oosterhof, Nikolaas N. and St-Jean, Samuel and {Bago " "Amirbekian} and Nimmo-Smith, Ian and {Satrajit Ghosh}},\n keywords = {},\n title = {nibabel " "2.0.1},\n publisher = {Zenodo},\n year = {2015}\n} " ) assert ( format_bibtex(BibTeX(bibtex_utf8)) == "Brรณtt, M. et al., 2015. nibabel 2.0.1." ) def test_is_contained() -> None: toppath = "package" assert _is_contained(toppath, "package.module") assert _is_contained(toppath, "package.module.submodule") assert _is_contained(toppath, "package.module.submodule:object") assert _is_contained(toppath, "package:object") assert _is_contained(toppath, toppath) assert not _is_contained(toppath, "package2") assert not _is_contained(toppath, "package2:anotherobject") assert not _is_contained(toppath, "package2.module:anotherobject") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_utils.py0000644000175100001770000000226714627677134021062 0ustar00runnerdocker# 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 # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from __future__ import annotations import sys from typing import Any from pytest import MonkeyPatch from ..utils import is_interactive def test_is_interactive_crippled_stdout(monkeypatch: MonkeyPatch) -> None: class MockedOut: """the one which has no isatty""" def write(self, *args: Any, **kwargs: Any) -> None: pass class MockedIsaTTY(MockedOut): def isatty(self) -> bool: return True for inout in ("in", "out", "err"): monkeypatch.setattr(sys, "std%s" % inout, MockedOut()) assert not is_interactive() # just for paranoids for inout in ("in", "out", "err"): monkeypatch.setattr(sys, "std%s" % inout, MockedIsaTTY()) assert is_interactive() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/tests/test_versions.py0000644000175100001770000000623114627677134021565 0ustar00runnerdocker# 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 __future__ import annotations from os import linesep import pytest from ..version import __version__ from ..versions import ExternalVersions, Version # just to ease testing def cmp(a, b): return (a > b) - (a < b) def test_external_versions_basic(): ev = ExternalVersions() assert ev._versions == {} assert ev["duecredit"] == Version(__version__) # and it could be compared assert ev["duecredit"] >= Version(__version__) assert ev["duecredit"] > Version("0.1") assert list(ev.keys()) == ["duecredit"] assert "duecredit" in ev assert "unknown" not in ev # Version might remove training .0 version_str = ( str(ev["duecredit"]) if isinstance(ev["duecredit"], Version) else __version__ ) assert ev.dumps() == "Versions: duecredit=%s" % version_str # For non-existing one we get None assert ev["duecreditnonexisting"] is None # and nothing gets added to _versions for nonexisting assert set(ev._versions.keys()) == {"duecredit"} # but if it is a module without version, we get it set to UNKNOWN assert ev["os"] == ev.UNKNOWN # And get a record on that inside assert 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) with pytest.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 ev[mod] == Version(mod.__version__) # Check that we can get a copy of the versions versions_dict = ev.versions versions_dict["duecredit"] = "0.0.1" assert versions_dict["duecredit"] == "0.0.1" assert ev["duecredit"] == Version(__version__) def test_external_versions_unknown() -> None: assert str(ExternalVersions.UNKNOWN) == "UNKNOWN" def _test_external(ev, modname: str) -> None: try: exec("import %s" % modname, globals(), locals()) except ImportError: modname = pytest.importorskip(modname) except Exception as e: pytest.skip("External {} fails to import: {}".format(modname, e)) assert ev[modname] is not ev.UNKNOWN assert ev[modname] > Version("0.0.1") assert Version("1000000.0") > ev[modname] # unlikely in our lifetimes @pytest.mark.parametrize( "modname", [ "scipy", "numpy", "mvpa2", "sklearn", "statsmodels", "pandas", "matplotlib", "psychopy", ], ) def test_external_versions_popular_packages(modname: str) -> None: ev = ExternalVersions() _test_external(ev, modname) # more of a smoke test assert linesep not in ev.dumps() assert ev.dumps(indent=True).endswith(linesep) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/utils.py0000644000175100001770000002324214627677134016655 0ustar00runnerdocker# 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 # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from __future__ import annotations from functools import wraps import logging import os from os.path import abspath, exists, expanduser, expandvars, isabs from os.path import join as opj import platform import shutil import stat import sys import time from types import TracebackType from typing import Any # # Some useful variables # platform_system = platform.system() on_windows = platform_system == "Windows" on_osx = platform_system == "Darwin" on_linux = platform_system == "Linux" lgr = logging.getLogger("duecredit.utils") # # Little helpers # def is_interactive() -> bool: """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: str, force_absolute: bool = True) -> str: """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: str) -> bool: """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: str, ro: bool = True, chmod_files: bool = True) -> None: """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: def chmod(f: str) -> None: os.chmod(f, os.stat(f).st_mode & ~stat.S_IWRITE) else: def chmod(f: str) -> None: os.chmod(f, os.stat(f).st_mode | stat.S_IWRITE | stat.S_IREAD) for root, _, 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: str, chmod_files: str | bool = "auto", *args: Any, **kwargs: Any ) -> None: """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 else: assert type(chmod_files) is bool chmod_files_ = chmod_files 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: str, *args: Any, **kwargs: Any) -> None: """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: if i < 9: time.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: str | None = None, replace: str | None = 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: dict[str, str] | None = None, prefix: str = "", wrapped=None ) -> dict[str, str]: """Updates kwargs to be passed to tempfile. calls depending on env vars""" # operate on a copy of tkwargs to avoid any side-effects if tkwargs is None: tkwargs = {} tkwargs_ = tkwargs.copy() # TODO: don't remember why I had this one originally # if len(targs)<2 and \ if "prefix" not 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_ # # Context Managers # # # Additional handlers # _sys_excepthook = sys.excepthook # Just in case we ever need original one def setup_exceptionhook() -> None: """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( exc_type: type[BaseException], exc_value: BaseException, exc_tb: TracebackType | None = None, ) -> None: if is_interactive(): import pdb import traceback traceback.print_exception(exc_type, exc_value, exc_tb) print pdb.post_mortem(exc_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__(exc_type, exc_value, exc_tb) _sys_excepthook(exc_type, exc_value, exc_tb) sys.excepthook = _duecredit_pdb_excepthook ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit/version.py0000644000175100001770000000010214627677151017167 0ustar00runnerdocker__version__ = '0.10.2' __release_date__ = 'Jun 04 2024, 20:51:53' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/duecredit/versions.py0000644000175100001770000001135514627677134017367 0ustar00runnerdocker# 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 __future__ import annotations """Module to help maintain a registry of versions for external modules etc """ from collections.abc import KeysView from importlib.metadata import version as metadata_version from os import linesep import sys from types import ModuleType from typing import Any from looseversion import LooseVersion from packaging.version import Version # To depict an unknown version, which can't be compared by mistake etc class UnknownVersion: """For internal use""" def __str__(self) -> str: return "UNKNOWN" def __eq__(self, other: Any) -> bool: if other is self: return True raise TypeError("UNKNOWN version is not comparable") class ExternalVersions: """Helper to figure out/use versions of the external modules. It maintains a dictionary of `packaging.version.Version`s to make comparisons easy. If a version string doesn't conform to Version, LooseVersion will be used. If a version can't be deduced for a module, 'None' is assigned """ UNKNOWN = UnknownVersion() def __init__(self) -> None: self._versions: dict[str, Version | LooseVersion | UnknownVersion] = {} @classmethod def _deduce_version( klass, module: ModuleType ) -> Version | LooseVersion | UnknownVersion: 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 not version: # Try importlib.metadata # module name might be different, and I found no way to # deduce it for citeproc which comes from "citeproc-py" # distribution modname = module.__name__ try: version = metadata_version( {"citeproc": "citeproc-py"}.get(modname, modname) ) except Exception: pass # oh well - no luck either if version: try: return Version(version) except ValueError: # let's then go with Loose one return LooseVersion(version) else: return klass.UNKNOWN def __getitem__( self, module: Any ) -> Version | LooseVersion | UnknownVersion | None: # when ran straight in its source code -- fails to discover nipy's version.. TODO # if module == 'nipy': if not isinstance(module, str): 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) -> KeysView[str]: """Return names of the known modules""" return self._versions.keys() def __contains__(self, item: str) -> bool: return item in self._versions @property def versions(self) -> dict[str, Version | LooseVersion | UnknownVersion]: """Return dictionary (copy) of versions""" return self._versions.copy() def dumps(self, indent: bool | str = False, preamble: str = "Versions:") -> str: """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 """ items = ["{}={}".format(k, self._versions[k]) for k in sorted(self._versions)] out = "%s" % preamble if indent: indent_ = " " if indent is True else indent out += (linesep + indent_).join([""] + items) + linesep else: out += " " + " ".join(items) return out external_versions = ExternalVersions() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/duecredit.egg-info/0000755000175100001770000000000014627677152016632 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/PKG-INFO0000644000175100001770000004073114627677151017733 0ustar00runnerdockerMetadata-Version: 2.1 Name: duecredit Version: 0.10.2 Summary: Publications (and donations) tracer Home-page: https://github.com/duecredit/duecredit Author: Yaroslav Halchenko, Matteo Visconti di Oleggio Castello Author-email: yoh@onerussian.com License: 2-clause BSD License Keywords: citation tracing Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Environment :: Other Environment Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Legal Industry Classifier: Intended Audience :: Other Audience Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Documentation Classifier: Topic :: Software Development :: Documentation Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: citeproc-py>=0.4 Requires-Dist: looseversion Requires-Dist: packaging Requires-Dist: requests Provides-Extra: tests Requires-Dist: pytest; extra == "tests" Requires-Dist: pytest-cov; extra == "tests" Requires-Dist: vcrpy; extra == "tests" Requires-Dist: contextlib2; extra == "tests" # duecredit [![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) [![DOI](https://zenodo.org/badge/DOI/110.5281/zenodo.3376260.svg)](https://doi.org/10.5281/zenodo.3376260) [![PyPI version fury.io](https://badge.fury.io/py/duecredit.svg)](https://pypi.python.org/pypi/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. ## Installation Duecredit is easy to install via pip, simply type: `pip install duecredit` ## Examples ### To cite the modules and methods you are using You can already start "registering" citations using duecredit in your Python modules and even registering citations (we call this approach "injections") for modules that do not (yet) use duecredit. duecredit will remain an optional dependency, i.e. your software will work correctly even without duecredit installed. For example, list citations of the modules and methods `yourproject` uses with a few simple commands: ```bash cd /path/to/yourmodule # for ~/yourproject cd yourproject # change directory into where the main code base is python -m duecredit yourproject.py ``` Or you can also display them in BibTex format, using: ```bash duecredit summary --format=bibtex ``` See this gif animation for a better illustration: ![Example](examples/duecredit_example.gif) ### To let others cite your software 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 a generic 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") By default, the added reference does not show up in the summary report (but see the `User-view` section below). If your reference is to a core package and you find that it should be listed in the summary then set `cite_module=True` (see [here](https://github.com/duecredit/duecredit/blob/master/duecredit/collector.py#L35) for a complete description of the arguments) due.cite(Doi("1.2.3/x.y.z"), description="The Answer to Everything", path="magicpy", cite_module=True) Similarly, to provide a direct reference for a function or a method, use the `dcite` decorator (by default this decorator sets cite_module=True) @due.dcite(Doi("1.2.3/x.y.z"), description="Resolves constipation issue") def pushit(): ... You can easily obtain a DOI for your software using Zenodo.org and a few other DOI providers. 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") ## Now what ### Do the due Once you obtained the references in the duecredit output, include them in in the references section of your paper or software. ### 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 the current directory or to your `DUECREDIT_FILE` environment setting: $> 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. ... ## Tags You are welcome to introduce new tags specific to your citations but we hope that for consistency across projects, you would use the following tags - `implementation` (default) โ€” an implementation of the cited method - `reference-implementation` โ€” the original implementation (ideally by the authors of the paper) of the cited method - `another-implementation` โ€” some other implementation of the method, e.g. if you would like to provide a citation for another implementation of the method you have implemented in your code and for which you have already provided `implementation` or `reference-implementation` tag - `use` โ€” publications demonstrating a worthwhile noting use of the method - `edu` โ€” tutorials, textbooks and other materials useful to learn more about cited functionality - `donate` โ€” should be commonly used with URL entries to point to the websites describing how to contribute some funds to the referenced project - `funding` โ€” to point to the sources of funding which provided support for a given functionality implementation and/or method development - `dataset` - for datasets ## 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 an 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 hand. **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. [citepy](https://github.com/clbarnes/citepy) -- Easily cite software libraries using information from automatically gathered from their package repository. ## Currently used by This is a running list of projects that use DueCredit natively. If you are using DueCredit, or plan to use it, please consider sending a pull request and add your project to this list. Thanks to [@fedorov](https://github.com/fedorov) for the idea. - [PyMVPA](http://www.pymvpa.org) - [fatiando](https://github.com/fatiando/fatiando) - [Nipype](https://github.com/nipy/nipype) - [QInfer](https://github.com/QInfer/python-qinfer) - [shablona](https://github.com/uwescience/shablona) - [gfusion](https://github.com/mvdoc/gfusion) - [pybids](https://github.com/INCF/pybids) - [Quickshear](https://github.com/nipy/quickshear) - [meqc](https://github.com/emdupre/meqc) - [MDAnalysis](https://www.mdanalysis.org) - [bctpy](https://github.com/aestrivex/bctpy) - [TorchIO](https://github.com/fepegar/torchio) - [BIDScoin](https://github.com/Donders-Institute/bidscoin) Last updated 2024-02-23. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/SOURCES.txt0000644000175100001770000000345214627677151020521 0ustar00runnerdockerCHANGELOG.md CONTRIBUTING.md LICENSE MANIFEST.in README.md requirements.txt setup.cfg setup.py tox.ini duecredit/__init__.py duecredit/__main__.py duecredit/collector.py duecredit/config.py duecredit/dueswitch.py duecredit/entries.py duecredit/io.py duecredit/log.py duecredit/parsers.py duecredit/stub.py duecredit/utils.py duecredit/version.py duecredit/versions.py duecredit.egg-info/PKG-INFO duecredit.egg-info/SOURCES.txt duecredit.egg-info/dependency_links.txt duecredit.egg-info/entry_points.txt duecredit.egg-info/requires.txt duecredit.egg-info/top_level.txt duecredit/cmdline/__init__.py duecredit/cmdline/cmd_summary.py duecredit/cmdline/cmd_test.py duecredit/cmdline/common_args.py duecredit/cmdline/helpers.py duecredit/cmdline/main.py duecredit/injections/__init__.py duecredit/injections/injector.py duecredit/injections/mod_biosig.py duecredit/injections/mod_dipy.py duecredit/injections/mod_matplotlib.py duecredit/injections/mod_mdp.py duecredit/injections/mod_mne.py duecredit/injections/mod_nibabel.py duecredit/injections/mod_nipy.py duecredit/injections/mod_nipype.py duecredit/injections/mod_numpy.py duecredit/injections/mod_pandas.py duecredit/injections/mod_psychopy.py duecredit/injections/mod_scipy.py duecredit/injections/mod_skimage.py duecredit/injections/mod_sklearn.py duecredit/tests/__init__.py duecredit/tests/test__main__.py duecredit/tests/test_api.py duecredit/tests/test_cmdline.py duecredit/tests/test_collector.py duecredit/tests/test_dueswitch.py duecredit/tests/test_entries.py duecredit/tests/test_import_doi duecredit/tests/test_injections.py duecredit/tests/test_io.py duecredit/tests/test_utils.py duecredit/tests/test_versions.py duecredit/tests/envs/nolxml/lxml.py duecredit/tests/mod/__init__.py duecredit/tests/mod/imported.py duecredit/tests/mod/submod.py examples/example_scipy.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/dependency_links.txt0000644000175100001770000000000114627677151022677 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/entry_points.txt0000644000175100001770000000007214627677151022126 0ustar00runnerdocker[console_scripts] duecredit = duecredit.cmdline.main:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/requires.txt0000644000175100001770000000013614627677151021231 0ustar00runnerdockerciteproc-py>=0.4 looseversion packaging requests [tests] pytest pytest-cov vcrpy contextlib2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534313.0 duecredit-0.10.2/duecredit.egg-info/top_level.txt0000644000175100001770000000001214627677151021354 0ustar00runnerdockerduecredit ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/examples/0000755000175100001770000000000014627677152015006 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/examples/example_scipy.py0000644000175100001770000000076714627677134020234 0ustar00runnerdocker# 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/requirements.txt0000644000175100001770000000014714627677134016456 0ustar00runnerdocker# install everything among install_requires specified in setup.py # and needed for testing -e .[tests] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717534313.609193 duecredit-0.10.2/setup.cfg0000644000175100001770000000026314627677152015012 0ustar00runnerdocker[bdist_rpm] release = 1 packager = Yaroslav Halchenko doc_files = README.md CHANGELOG.md LICENSE CONTRIBUTING.md [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/setup.py0000755000175100001770000001125314627677134014707 0ustar00runnerdocker#!/usr/bin/env python """ duecredit -- publications (donations, etc) tracer """ from datetime import datetime import os.path import re from subprocess import PIPE, Popen import sys from setuptools import find_packages, setup # Adopted from citeproc-py # License: BSD-2 # Copyright 2011-2013 Brecht Machiels PACKAGE = "duecredit" VERSION_FILE = PACKAGE + "/version.py" def make_pep440_compliant(version: str, local_prefix: str) -> str: """Convert the version into a PEP440 compliant version.""" public_version_re = re.compile( r"^([0-9][0-9.]*(?:(?:a|b|rc|.post|.dev)[0-9]+)*)\+?" ) _, public, local = public_version_re.split(version, maxsplit=1) if not local: return version sanitized_local = re.sub("[+~-]+", ".", local).strip(".") pep440_version = f"{public}+{local_prefix}{sanitized_local}" assert re.match( "^[a-zA-Z0-9.]+$", sanitized_local ), f"'{pep440_version}' not PEP440 compliant" return pep440_version # retrieve the version number from git or VERSION_FILE # inspired by http://dcreager.net/2010/02/10/setuptools-git-version-numbers/ try: if os.path.exists("debian/copyright"): print("Generating version.py out of debian/copyright information") # building debian package. Deduce version from debian/copyright with open("debian/changelog") as f: lines = f.readlines() __version__ = make_pep440_compliant(lines[0].split()[1].strip("()"), "debian.") # TODO: unify format whenever really bored ;) __release_date__ = re.sub( r"^ -- .*>\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 assert git.stdout 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(f"__version__ = '{__version__}'\n") version_file.write(f"__release_date__ = '{__release_date__}'\n") except OSError: 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", encoding="utf-8") as f: README = f.read() setup( name=PACKAGE, version=__version__, packages=find_packages(), python_requires=">=3.8", install_requires=[ "citeproc-py>=0.4", "looseversion", "packaging", "requests", ], extras_require={"tests": ["pytest", "pytest-cov", "vcrpy", "contextlib2"]}, include_package_data=True, 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=README, long_description_content_type="text/markdown", url="https://github.com/duecredit/duecredit", 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 :: 3", "Topic :: Documentation", "Topic :: Software Development :: Documentation", "Topic :: Software Development :: Libraries :: Python Modules", ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717534300.0 duecredit-0.10.2/tox.ini0000644000175100001770000000242114627677134014502 0ustar00runnerdocker[tox] envlist = flake8,py3,typing [testenv] commands = pytest {posargs} duecredit deps = -r{toxinidir}/requirements.txt [testenv:flake8] deps = flake8 flake8-bugbear flake8-builtins flake8-unused-arguments commands = flake8 {posargs} [testenv:venv] commands = {posargs} [testenv:typing] deps = mypy types-requests commands = mypy --ignore-missing-imports {posargs} duecredit/ [flake8] doctests = True extend-exclude = .venv,venv-debug,venvs,build,dist,doc,git/ext/ max-line-length = 120 unused-arguments-ignore-stub-functions = True extend-select = B901,B902,B950 extend-ignore = A003,E203,E501,U101 [isort] atomic = True force_sort_within_sections = True honor_noqa = True lines_between_sections = 1 profile = black reverse_relative = True sort_relative_in_force_sorted_sections = True known_first_party = duecredit [pytest] addopts = --cov=duecredit # Explicitly setting the path to the coverage config file is necessary due # to some tests spawning subprocesses with changed working directories. --cov-config=tox.ini --tb=short --durations=10 filterwarnings = # TODO: review/address all warnings # error [coverage:run] branch = True parallel = True [coverage:paths] source = duecredit .tox/**/site-packages/duecredit