Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process reference #30

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions r2t2/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspect
import wrapt
from typing import NamedTuple, List
from typing import NamedTuple, List, Optional, Callable
from functools import reduce


Expand All @@ -22,7 +22,7 @@ def add_record(out, record):
out += f"\nSource file: {record.source}"
out += f"\nLine: {record.line}\n"
for short, ref in zip(record.short_purpose, record.references):
out += f"\t[{index}] {short} - {ref}\n"
out += f"\t[{index}] {short} - {ref.split(']', 1)[-1]}\n"
index += 1
out += "\n"
return out
Expand All @@ -46,36 +46,49 @@ def tracking(self, enabled=True):
BIBLIOGRAPHY: Biblio = Biblio()


def add_reference(*, short_purpose: str, reference: str):
def add_reference(
*, short_purpose: str, reference: Optional[str] = None, doi: Optional[str] = None
) -> Callable:
"""Decorator to link a reference to a function or method.

Acts as a marker in code where particular alogrithms/data/... originates.
General execution of code silently passes these markers, but remembers how and where
they were called. Which markers were passed in a particular program run
can be recalled with print_references().
can be recalled with `print(BIBLIOGRAPHY)`.

Arguments:
short_purpose: Identify the thing being referenced (string)
reference: The reference itself, in any sensible format.
One and only one method for providing the reference is allowed.

Args:
short_purpose (str): Identify the thing being referenced.
reference (Optional, str): The reference itself, as a plain text string.
doi (Optional, str): DOI of the reference.

Returns:
The decorated function.
"""
if reference and doi:
raise ValueError("Only one method for providing the reference is allowed.")
elif reference:
ref = f"[plain]{reference}"
elif doi:
ref = f"[doi]{doi}" if "doi.org" in doi else f"[doi]https://doi.org/{doi}"
else:
raise ValueError("No reference information provided!")

@wrapt.decorator(enabled=lambda: BIBLIOGRAPHY.track_references)
def wrapper(wrapped, instance, args, kwargs):
source = inspect.getsourcefile(wrapped)
line = inspect.getsourcelines(wrapped)[1]
identifier = f"{source}:{line}"

if (
identifier in BIBLIOGRAPHY
and reference in BIBLIOGRAPHY[identifier].references
):
if identifier in BIBLIOGRAPHY and ref in BIBLIOGRAPHY[identifier].references:
return wrapped(*args, **kwargs)

if identifier not in BIBLIOGRAPHY:
BIBLIOGRAPHY[identifier] = FunctionReference(wrapped.__name__, line, source)

BIBLIOGRAPHY[identifier].short_purpose.append(short_purpose)
BIBLIOGRAPHY[identifier].references.append(reference)
BIBLIOGRAPHY[identifier].references.append(ref)

return wrapped(*args, **kwargs)

Expand Down
66 changes: 66 additions & 0 deletions r2t2/reference_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Callable, Dict
from functools import partial, lru_cache
from abc import ABC, abstractmethod
import re


PROCESSORS: Dict[str, Callable] = {}
"""Dictionary of available processors."""


def register_processor(f: Callable = None, name: str = None) -> Callable:
"""Register a reference processor.

Args:
f (callable): Function processing the reference string.
name (str): Name of the processor.

Returns:
The input processor.
"""
if f is None:
return partial(register_processor, name=name)

name = name if name else f.__name__
PROCESSORS[name] = f

return f


@lru_cache
def process_reference(ref: str, output_format: str = "plain"):
"""Process the reference string in the chosen format.

This function is cached, so multiple calls with identical arguments are processed
faster, specially useful when information needs to be read from a file or retrieved
from the internet.

Args:
ref (str): The reference string. Should start with [XXX], where XXX is one of
the known processors.
output_format (str): The format the reference should be provided
("plain" or "bibtex").

Returns:
The reference in the requested format.
"""
match = re.findall(r'([\w.-/\\:\s]+)', ref)
if len(match) != 2:
raise ValueError(f"Could not process reference {ref}")

return getattr(PROCESSORS[match[0]](match[1]), output_format)


class ProcessorBase(ABC):
"""Base class for all reference processors."""

def __init__(self, ref):
self._ref = ref

@abstractmethod
def plain(self) -> str:
"""Return the reference as plain text in a sensible format."""

@abstractmethod
def bibtex(self) -> str:
"""Return the reference as a bibtex entry."""
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pytest import fixture


@fixture
def decorated_function():
from r2t2 import add_reference

@add_reference(
short_purpose="Roasted chicken recipe", reference="Great British Roasts, 2019"
)
def roasted_chicken(ingredients=None):
pass

return roasted_chicken


@fixture
def decorated_with_doi():
from r2t2 import add_reference

@add_reference(short_purpose="DOI reference", doi="10.5281/zenodo.1185316")
def a_great_function():
pass

return a_great_function
39 changes: 39 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
def test_add_reference(decorated_function):
from r2t2 import BIBLIOGRAPHY

assert "[plain]Great British Roasts, 2019" not in BIBLIOGRAPHY.references
decorated_function()
assert "[plain]Great British Roasts, 2019" not in BIBLIOGRAPHY.references

BIBLIOGRAPHY.tracking()
decorated_function("Chicken")
assert "[plain]Great British Roasts, 2019" in BIBLIOGRAPHY.references

BIBLIOGRAPHY.clear()
BIBLIOGRAPHY.tracking(False)


def test_print_references(capsys, decorated_function):
from r2t2 import BIBLIOGRAPHY

BIBLIOGRAPHY.tracking()
decorated_function("Chicken")
print(BIBLIOGRAPHY)
captured = capsys.readouterr()

assert "Great British Roasts, 2019" in captured.out

BIBLIOGRAPHY.clear()
BIBLIOGRAPHY.tracking(False)


def test_add_reference_from_doi(decorated_with_doi):
from r2t2 import BIBLIOGRAPHY

BIBLIOGRAPHY.tracking()

decorated_with_doi()
assert "[doi]https://doi.org/" in BIBLIOGRAPHY.references[-1]

BIBLIOGRAPHY.clear()
BIBLIOGRAPHY.tracking(False)
41 changes: 0 additions & 41 deletions tests/test_r2t2.py

This file was deleted.