diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5f274a07..a7c5027c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -4,6 +4,7 @@ name: Python package on: + workflow_dispatch: push: branches: [ master ] tags: [ '*' ] @@ -23,13 +24,13 @@ jobs: outputs: version: ${{ steps.get_version.outputs.version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-cache-v1 @@ -39,7 +40,7 @@ jobs: - name: Build sdist and wheel run: python -m build - run: twine check dist/* - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: dist path: dist/ @@ -73,20 +74,20 @@ jobs: TEMPLATEFLOW_HOME: /tmp/home THISVERSION: ${{ needs.build.outputs.version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: matrix.mode == 'repo' || matrix.mode == 'editable' with: fetch-depth: 0 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 if: matrix.mode == 'sdist' || matrix.mode == 'wheel' with: name: dist path: /tmp/package/ - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-cache-v1 diff --git a/templateflow/api.py b/templateflow/api.py index ecef3af7..eadb07eb 100644 --- a/templateflow/api.py +++ b/templateflow/api.py @@ -333,7 +333,12 @@ def _to_bibtex(doi, template, idx): ) return doi - return response.text + # doi.org may not honor requested charset, to safeguard force a bytestream with + # response.content, then decode into UTF-8. + bibtex = response.content.decode() + + # doi.org / crossref may still point to the no longer preferred proxy service + return bibtex.replace('http://dx.doi.org/', 'https://doi.org/') def _normalize_ext(value): diff --git a/templateflow/tests/test_api.py b/templateflow/tests/test_api.py index 8e66dc85..de8cd951 100644 --- a/templateflow/tests/test_api.py +++ b/templateflow/tests/test_api.py @@ -1,9 +1,77 @@ """Test citations.""" + import pytest from .. import api +class Bibtex: + def __init__(self, bibtex): + self.text = bibtex + self.url_only = False + self.etype = None + self.citekey = None + self.pairs = {} + + # DOI could not be converted + if self.text.startswith("http"): + self.url_only = True + else: + self._parse_bibtex() + + def _parse_bibtex(self): + import re + + try: + self.etype = re.search(r"@(\w+)", self.text).group(1) + except AttributeError: + raise TypeError(f"Invalid bibtex: {self.text}") + try: + self.citekey = re.search(r"@[^{]*{([^,\s]+)", self.text).group(1) + except AttributeError: + raise TypeError(f"Invalid bibtex: {self.text}") + self.pairs = { + key: val for key, val in re.findall(r"(\w+)=(\{[^{}]+\})", self.text) + } + + def get(self, val): + return self.pairs.get(val) + + def __str__(self): + return self.text + + def __repr__(self): + return f'@{self.etype}{{{self.citekey}, {", ".join([f"{key} = {val}" for key, val in self.pairs.items()])}}}' + + def __eq__(self, other): + if isinstance(other, Bibtex): + if self.url_only and self.text == other.text: + return True + if ( + self.etype == other.etype + and self.citekey == other.citekey + and self.pairs == other.pairs + ): + return True + return False + + def assert_same(self, other): + """Convenience method to find deviations between two Bibtex objects""" + assert isinstance(other, Bibtex) + assert self.etype == other.etype, "Mismatched entry types" + assert self.citekey == other.citekey, "Mismatched citekeys" + for key in self.pairs.keys(): + assert key in other.pairs, f"Key ({key}) missing from other" + assert ( + self.pairs[key] == other.pairs[key] + ), f"Key ({key}) mismatched\n\n{self.pairs[key]}\n\n{other.pairs[key]}" + + for key in other.pairs.keys(): + assert key in self.pairs, f"Key ({key}) missing from pairs" + + assert self.pairs == other.pairs, "Dictionaries do not match" + + # test setup to avoid cluttering pytest parameterize mni2009_urls = [ "https://doi.org/10.1016/j.neuroimage.2010.07.033", @@ -14,30 +82,32 @@ mni2009_fbib = """\ @article{Fonov_2011, -\tdoi = {10.1016/j.neuroimage.2010.07.033}, -\turl = {https://doi.org/10.1016%2Fj.neuroimage.2010.07.033}, -\tyear = 2011, -\tmonth = {jan}, -\tpublisher = {Elsevier {BV}}, -\tvolume = {54}, -\tnumber = {1}, -\tpages = {313--327}, -\tauthor = {Vladimir Fonov and Alan C. Evans and Kelly Botteron and C. Robert \ -Almli and Robert C. McKinstry and D. Louis Collins}, -\ttitle = {Unbiased average age-appropriate atlases for pediatric studies}, -\tjournal = {{NeuroImage}} +DOI={10.1016/j.neuroimage.2010.07.033}, +url={https://doi.org/10.1016/j.neuroimage.2010.07.033}, +year={2011}, +publisher={Elsevier BV}, +ISSN={1053-8119}, +volume={54}, +number={1}, +pages={313–327}, +author={Fonov, Vladimir and Evans, Alan C. and Botteron, Kelly and Almli, C. Robert \ +and McKinstry, Robert C. and Collins, D. Louis}, +title={Unbiased average age-appropriate atlases for pediatric studies}, +journal={NeuroImage} }""" mni2009_lbib = """\ -@incollection{Collins_1999, -\tdoi = {10.1007/3-540-48714-x_16}, -\turl = {https://doi.org/10.1007%2F3-540-48714-x_16}, -\tyear = 1999, -\tpublisher = {Springer Berlin Heidelberg}, -\tpages = {210--223}, -\tauthor = {D. Louis Collins and Alex P. Zijdenbos and Wim F. C. Baar{\\'{e}} and Alan C. Evans}, -\ttitle = {{ANIMAL}$\\mathplus${INSECT}: Improved Cortical Structure Segmentation}, -\tbooktitle = {Lecture Notes in Computer Science} +@inbook{Collins_1999, +DOI={10.1007/3-540-48714-x_16}, +url={https://doi.org/10.1007/3-540-48714-X_16}, +year={1999}, +publisher={Springer Berlin Heidelberg}, +pages={210–223}, +ISBN={9783540487142}, +ISSN={0302-9743} +author={Collins, D. Louis and Zijdenbos, Alex P. and Baaré, Wim F. C. and Evans, Alan C.}, +title={ANIMAL+INSECT: Improved Cortical Structure Segmentation}, +booktitle={Information Processing in Medical Imaging} }""" fslr_urls = [ @@ -47,18 +117,18 @@ fslr_fbib = """\ @article{Van_Essen_2011, -\tdoi = {10.1093/cercor/bhr291}, -\turl = {https://doi.org/10.1093%2Fcercor%2Fbhr291}, -\tyear = 2011, -\tmonth = {nov}, -\tpublisher = {Oxford University Press ({OUP})}, -\tvolume = {22}, -\tnumber = {10}, -\tpages = {2241--2262}, -\tauthor = {D. C. Van Essen and M. F. Glasser and D. L. Dierker and J. Harwell and T. Coalson}, -\ttitle = {Parcellations and Hemispheric Asymmetries of Human Cerebral Cortex Analyzed on \ +DOI={10.1093/cercor/bhr291}, +ISSN={1460-2199}, +url={https://doi.org/10.1093/cercor/bhr291}, +year={2011}, +publisher={Oxford University Press (OUP)}, +volume={22}, +number={10}, +pages={2241–2262}, +author={Van Essen, D. C. and Glasser, M. F. and Dierker, D. L. and Harwell, J. and Coalson, T.}, +title={Parcellations and Hemispheric Asymmetries of Human Cerebral Cortex Analyzed on \ Surface-Based Atlases}, -\tjournal = {Cerebral Cortex} +journal={Cerebral Cortex} }""" fslr_lbib = ( @@ -67,24 +137,33 @@ fsaverage_fbib = """\ @article{Fischl_1999, -\tdoi = {10.1002/(sici)1097-0193(1999)8:4<272::aid-hbm10>3.0.co;2-4}, -\turl = {https://doi.org/10.1002%2F%28sici%291097-0193%281999%298%3A4%3C272%3A%3Aaid-hbm10%3E3.0.co%3B2-4}, -\tyear = 1999, -\tpublisher = {Wiley}, -\tvolume = {8}, -\tnumber = {4}, -\tpages = {272--284}, -\tauthor = {Bruce Fischl and Martin I. Sereno and Roger B.H. Tootell and Anders M. Dale}, -\ttitle = {High-resolution intersubject averaging and a coordinate system for the cortical surface}, -\tjournal = {Human Brain Mapping} +DOI={10.1002/(sici)1097-0193(1999)8:4<272::aid-hbm10>3.0.co;2-4}, +ISSN={1097-0193}, +url={https://doi.org/10.1002/(sici)1097-0193(1999)8:4<272::aid-hbm10>3.0.co;2-4}, +year={1999}, +publisher={Wiley}, +volume={8}, +number={4}, +pages={272–284}, +author={Fischl, Bruce and Sereno, Martin I. and Tootell, Roger B.H. and Dale, Anders M.}, +title={High-resolution intersubject averaging and a coordinate system for the cortical surface}, +journal={Human Brain Mapping} }""" + @pytest.mark.parametrize( "template,urls,fbib,lbib", [ ("MNI152NLin2009cAsym", mni2009_urls, mni2009_fbib, mni2009_lbib), ("fsLR", fslr_urls, fslr_fbib, fslr_lbib), - ("fsaverage", ["https://doi.org/10.1002/(sici)1097-0193(1999)8:4%3C272::aid-hbm10%3E3.0.co;2-4"], fsaverage_fbib, None), + ( + "fsaverage", + [ + "https://doi.org/10.1002/(sici)1097-0193(1999)8:4%3C272::aid-hbm10%3E3.0.co;2-4" + ], + fsaverage_fbib, + None, + ), ], ) def test_citations(tmp_path, template, urls, fbib, lbib): @@ -92,8 +171,16 @@ def test_citations(tmp_path, template, urls, fbib, lbib): assert api.get_citations(template) == urls bibs = api.get_citations(template, bibtex=True) if bibs: - assert "".join(bibs[0]) == fbib - assert len(bibs) == 1 if lbib is None else "".join(bibs[-1]) == lbib + bib0 = Bibtex(bibs[0]) + exp0 = Bibtex(fbib) + assert bib0 == exp0 + if lbib is not None: + bib1 = Bibtex(bibs[-1]) + exp1 = Bibtex(lbib) + assert bib1 == exp1 + else: + assert len(bibs) == 1 + else: # no citations currently assert False @@ -108,7 +195,7 @@ def test_pybids_magic_get(): with pytest.raises(TypeError): api.ls_atlases("MNI152NLin6ASym") - + # Existing layout.get_* should not be bubbled to the layout # (that means, raise an AttributeError instead of a BIDSEntityError) with pytest.raises(AttributeError):