From e26c606163fb804cdd75bf03cb72ad93d9fd2987 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Thu, 7 Nov 2013 22:39:19 +0100 Subject: [PATCH 01/48] endecoder implementation --- papers/endecoder.py | 67 +++++++++++++++++++ tests/test_endecoder.py | 141 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 papers/endecoder.py create mode 100644 tests/test_endecoder.py diff --git a/papers/endecoder.py b/papers/endecoder.py new file mode 100644 index 0000000..d264603 --- /dev/null +++ b/papers/endecoder.py @@ -0,0 +1,67 @@ +import color +import yaml + +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +try: + import pybtex.database.input.bibtex + import pybtex.database.input.bibtexml + import pybtex.database.input.bibyaml + import pybtex.database.output.bibyaml + +except ImportError: + print(color.dye('error', color.error) + + ": you need to install Pybtex; try running 'pip install " + "pybtex' or 'easy_install pybtex'") + exit(-1) + + +class EnDecoder(object): + """ Encode and decode content. + + Design choices: + * Has no interaction with disk. + * Incoming content is not trusted. + * Returned content must be correctly formatted (no one else checks). + * Failures raise ValueError + * encode_bibdata will try to recognize exceptions + """ + + decode_fmt = (pybtex.database.input.bibyaml, + pybtex.database.input.bibtex, + pybtex.database.input.bibtexml) + + def encode_metadata(self, metadata): + return yaml.safe_dump(metadata, allow_unicode=True, encoding='UTF-8', indent = 4) + + def decode_metadata(self, metadata_raw): + return yaml.safe_load(metadata_raw) + + def encode_bibdata(self, bibdata): + """Encode bibdata """ + s = StringIO.StringIO() + pybtex.database.output.bibyaml.Writer().write_stream(bibdata, s) + return s.getvalue() + + def decode_bibdata(self, bibdata_raw): + """""" + bibdata_rawutf8 = unicode(bibdata_raw) + for fmt in EnDecoder.decode_fmt: + try: + bibdata_stream = StringIO.StringIO(bibdata_rawutf8) + return self._decode_bibdata(bibdata_stream, fmt.Parser()) + except ValueError: + pass + raise ValueError('could not parse bibdata') + + def _decode_bibdata(self, bibdata_stream, parser): + try: + entry = parser.parse_stream(bibdata_stream) + if len(entry.entries) > 0: + return entry + except Exception: + pass + raise ValueError('could not parse bibdata') diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py new file mode 100644 index 0000000..855913b --- /dev/null +++ b/tests/test_endecoder.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +import unittest + +import yaml + +import testenv +from papers import endecoder + +bibyaml_raw0 = """entries: + Page99: + abstract: The importance of a Web page is an inherently subjective matter, + which depends on the readers interests, knowledge and attitudes. But there + is still much that can be said objectively about the relative importance + of Web pages. This paper describes PageRank, a mathod for rating Web pages + objectively and mechanically, effectively measuring the human interest + and attention devoted to them. We compare PageRank to an idealized random + Web surfer. We show how to efficiently compute PageRank for large numbers + of pages. And, we show how to apply PageRank to search and to user navigation. + author: + - first: Lawrence + last: Page + - first: Sergey + last: Brin + - first: Rajeev + last: Motwani + - first: Terry + last: Winograd + institution: Stanford InfoLab + month: November + note: Previous number = SIDL-WP-1999-0120 + number: 1999-66 + publisher: Stanford InfoLab + title: 'The PageRank Citation Ranking: Bringing Order to the Web.' + type: techreport + url: http://ilpubs.stanford.edu:8090/422/ + year: '1999' +""" + +bibtexml_raw0 = """ + + + + + Stanford InfoLab + The PageRank Citation Ranking: Bringing Order to the Web. + http://ilpubs.stanford.edu:8090/422/ + The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation. + 1999-66 + November + Previous number = SIDL-WP-1999-0120 + 1999 + Stanford InfoLab + + + Lawrence + Page + + + Sergey + Brin + + + Rajeev + Motwani + + + Terry + Winograd + + + + + + +""" + +bibtex_raw0 = """ +@techreport{ + Page99, + author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", + publisher = "Stanford InfoLab", + title = "The PageRank Citation Ranking: Bringing Order to the Web.", + url = "http://ilpubs.stanford.edu:8090/422/", + abstract = "The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation.", + number = "1999-66", + month = "November", + note = "Previous number = SIDL-WP-1999-0120", + year = "1999", + institution = "Stanford InfoLab" +} +""" + +metadata_raw0 = """external-document: null +notes: [] +tags: [search, network] +""" + +def compare_yaml_str(s1, s2): + if s1 == s2: + return True + else: + y1 = yaml.safe_load(s1) + y2 = yaml.safe_load(s2) + return y1 == y2 + + +class TestEnDecode(unittest.TestCase): + + def test_endecode_bibyaml(self): + + decoder = endecoder.EnDecoder() + entry = decoder.decode_bibdata(bibyaml_raw0) + bibyaml_output0 = decoder.encode_bibdata(entry) + + self.assertEqual(bibyaml_raw0, bibyaml_output0) + self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) + + def test_endecode_bibtexml(self): + + decoder = endecoder.EnDecoder() + entry = decoder.decode_bibdata(bibtexml_raw0) + bibyaml_output0 = decoder.encode_bibdata(entry) + + self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) + + def test_endecode_bibtex(self): + + decoder = endecoder.EnDecoder() + entry = decoder.decode_bibdata(bibtex_raw0) + bibyaml_output0 = decoder.encode_bibdata(entry) + + self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) + + def test_endecode_metadata(self): + + decoder = endecoder.EnDecoder() + entry = decoder.decode_metadata(metadata_raw0) + metadata_output0 = decoder.encode_metadata(entry) + + self.assertEqual(metadata_raw0, metadata_output0) + From 856cfa2a4f6f95db133400a70dd46f1473a32050 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 8 Nov 2013 01:17:54 +0100 Subject: [PATCH 02/48] moved test string fixtures to str_fixtures.py --- tests/str_fixtures.py | 88 ++++++++++++++++++++++++++++++++++++++++ tests/test_endecoder.py | 89 +---------------------------------------- 2 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 tests/str_fixtures.py diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py new file mode 100644 index 0000000..0730beb --- /dev/null +++ b/tests/str_fixtures.py @@ -0,0 +1,88 @@ +bibyaml_raw0 = """entries: + Page99: + abstract: The importance of a Web page is an inherently subjective matter, + which depends on the readers interests, knowledge and attitudes. But there + is still much that can be said objectively about the relative importance + of Web pages. This paper describes PageRank, a mathod for rating Web pages + objectively and mechanically, effectively measuring the human interest + and attention devoted to them. We compare PageRank to an idealized random + Web surfer. We show how to efficiently compute PageRank for large numbers + of pages. And, we show how to apply PageRank to search and to user navigation. + author: + - first: Lawrence + last: Page + - first: Sergey + last: Brin + - first: Rajeev + last: Motwani + - first: Terry + last: Winograd + institution: Stanford InfoLab + month: November + note: Previous number = SIDL-WP-1999-0120 + number: 1999-66 + publisher: Stanford InfoLab + title: 'The PageRank Citation Ranking: Bringing Order to the Web.' + type: techreport + url: http://ilpubs.stanford.edu:8090/422/ + year: '1999' +""" + +bibtexml_raw0 = """ + + + + + Stanford InfoLab + The PageRank Citation Ranking: Bringing Order to the Web. + http://ilpubs.stanford.edu:8090/422/ + The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation. + 1999-66 + November + Previous number = SIDL-WP-1999-0120 + 1999 + Stanford InfoLab + + + Lawrence + Page + + + Sergey + Brin + + + Rajeev + Motwani + + + Terry + Winograd + + + + + + +""" + +bibtex_raw0 = """ +@techreport{ + Page99, + author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", + publisher = "Stanford InfoLab", + title = "The PageRank Citation Ranking: Bringing Order to the Web.", + url = "http://ilpubs.stanford.edu:8090/422/", + abstract = "The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation.", + number = "1999-66", + month = "November", + note = "Previous number = SIDL-WP-1999-0120", + year = "1999", + institution = "Stanford InfoLab" +} +""" + +metadata_raw0 = """external-document: null +notes: [] +tags: [search, network] +""" \ No newline at end of file diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py index 855913b..2e31e76 100644 --- a/tests/test_endecoder.py +++ b/tests/test_endecoder.py @@ -6,94 +6,7 @@ import yaml import testenv from papers import endecoder -bibyaml_raw0 = """entries: - Page99: - abstract: The importance of a Web page is an inherently subjective matter, - which depends on the readers interests, knowledge and attitudes. But there - is still much that can be said objectively about the relative importance - of Web pages. This paper describes PageRank, a mathod for rating Web pages - objectively and mechanically, effectively measuring the human interest - and attention devoted to them. We compare PageRank to an idealized random - Web surfer. We show how to efficiently compute PageRank for large numbers - of pages. And, we show how to apply PageRank to search and to user navigation. - author: - - first: Lawrence - last: Page - - first: Sergey - last: Brin - - first: Rajeev - last: Motwani - - first: Terry - last: Winograd - institution: Stanford InfoLab - month: November - note: Previous number = SIDL-WP-1999-0120 - number: 1999-66 - publisher: Stanford InfoLab - title: 'The PageRank Citation Ranking: Bringing Order to the Web.' - type: techreport - url: http://ilpubs.stanford.edu:8090/422/ - year: '1999' -""" - -bibtexml_raw0 = """ - - - - - Stanford InfoLab - The PageRank Citation Ranking: Bringing Order to the Web. - http://ilpubs.stanford.edu:8090/422/ - The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation. - 1999-66 - November - Previous number = SIDL-WP-1999-0120 - 1999 - Stanford InfoLab - - - Lawrence - Page - - - Sergey - Brin - - - Rajeev - Motwani - - - Terry - Winograd - - - - - - -""" - -bibtex_raw0 = """ -@techreport{ - Page99, - author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", - publisher = "Stanford InfoLab", - title = "The PageRank Citation Ranking: Bringing Order to the Web.", - url = "http://ilpubs.stanford.edu:8090/422/", - abstract = "The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation.", - number = "1999-66", - month = "November", - note = "Previous number = SIDL-WP-1999-0120", - year = "1999", - institution = "Stanford InfoLab" -} -""" - -metadata_raw0 = """external-document: null -notes: [] -tags: [search, network] -""" +from str_fixtures import bibyaml_raw0, bibtexml_raw0, bibtex_raw0, metadata_raw0 def compare_yaml_str(s1, s2): if s1 == s2: From c4701953deb1cb622b41b5ef51dc3acfc1502867 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 8 Nov 2013 01:19:59 +0100 Subject: [PATCH 03/48] fake_env module for fake fs, fake input --- tests/fake_env.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/fake_env.py diff --git a/tests/fake_env.py b/tests/fake_env.py new file mode 100644 index 0000000..1acfcb3 --- /dev/null +++ b/tests/fake_env.py @@ -0,0 +1,162 @@ +import sys +import os +import shutil +import glob +import unittest +import pkgutil +import re + +import testenv +import fake_filesystem +import fake_filesystem_shutil +import fake_filesystem_glob + +from papers import color +from papers.p3 import io, input + + # code for fake fs + +real_os = os +real_open = open +real_file = file +real_shutil = shutil +real_glob = glob + + + +# def _mod_list(): +# ml = [] +# import papers +# for importer, modname, ispkg in pkgutil.walk_packages( +# path=papers.__path__, +# prefix=papers.__name__ + '.', +# onerror=lambda x: None): +# # HACK to not load textnote +# if not modname.startswith('papers.plugs.texnote'): +# ml.append((modname, __import__(modname, fromlist='dummy'))) +# return ml + + + +def create_fake_fs(module_list): + + fake_fs = fake_filesystem.FakeFilesystem() + fake_os = fake_filesystem.FakeOsModule(fake_fs) + fake_open = fake_filesystem.FakeFileOpen(fake_fs) + fake_shutil = fake_filesystem_shutil.FakeShutilModule(fake_fs) + fake_glob = fake_filesystem_glob.FakeGlobModule(fake_fs) + + fake_fs.CreateDirectory(fake_os.path.expanduser('~')) + + try: + __builtins__.open = fake_open + __builtins__.file = fake_open + except AttributeError: + __builtins__['open'] = fake_open + __builtins__['file'] = fake_open + + sys.modules['os'] = fake_os + sys.modules['shutil'] = fake_shutil + sys.modules['glob'] = fake_glob + + for md in module_list: + md.os = fake_os + md.shutil = fake_shutil + md.open = fake_open + md.file = fake_open + + return {'fs': fake_fs, + 'os': fake_os, + 'open': fake_open, + 'shutil': fake_shutil, + 'glob': fake_glob} + +def unset_fake_fs(module_list): + try: + __builtins__.open = real_open + __builtins__.file = real_file + except AttributeError: + __builtins__['open'] = real_open + __builtins__['file'] = real_file + + sys.modules['os'] = real_os + sys.modules['shutil'] = real_shutil + sys.modules['glob'] = real_glob + + for md in module_list: + md.os = real_os + md.shutil = real_shutil + md.open = real_open + md.file = real_file + + +def copy_dir(fs, real_dir, fake_dir = None): + """Copy all the data directory into the fake fs""" + if fake_dir is None: + fake_dir = real_dir + for filename in real_os.listdir(real_dir): + real_path = real_os.path.join(real_dir, filename) + fake_path = fs['os'].path.join(fake_dir, filename) + if real_os.path.isfile(real_path): + with real_open(real_path, 'r') as f: + fs['fs'].CreateFile(fake_path, contents=f.read()) + if real_os.path.isdir(real_path): + fs['fs'].CreateDirectory(fake_path) + copy_dir(fs, real_path, fake_path) + + + # redirecting output + +def redirect(f): + def newf(*args, **kwargs): + old_stderr, old_stdout = sys.stderr, sys.stdout + stdout = io.StringIO() + stderr = io.StringIO() + sys.stdout, sys.stderr = stdout, stderr + try: + return f(*args, **kwargs), stdout, stderr + finally: + sys.stderr, sys.stdout = old_stderr, old_stdout + return newf + + +# Test helpers + +# automating input + +real_input = input + + +class FakeInput(): + """ Replace the input() command, and mock user input during tests + + Instanciate as : + input = FakeInput(['yes', 'no']) + then replace the input command in every module of the package : + input.as_global() + Then : + input() returns 'yes' + input() returns 'no' + input() raise IndexError + """ + + def __init__(self, module_list, inputs=None): + self.inputs = list(inputs) or [] + self.module_list = module_list + self._cursor = 0 + + def as_global(self): + for md in module_list: + md.input = self + md.editor_input = self + # if mdname.endswith('files'): + # md.editor_input = self + + def add_input(self, inp): + self.inputs.append(inp) + + def __call__(self, *args, **kwargs): + inp = self.inputs[self._cursor] + self._cursor += 1 + return inp + From c1bf80fe68a4eb53c9cb57ff2bee932a7b52820d Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 8 Nov 2013 01:21:15 +0100 Subject: [PATCH 04/48] filebroker class + tests --- papers/filebroker.py | 98 ++++++++++++++++++++++++++++++++++++++++ tests/test_filebroker.py | 45 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 papers/filebroker.py create mode 100644 tests/test_filebroker.py diff --git a/papers/filebroker.py b/papers/filebroker.py new file mode 100644 index 0000000..0517772 --- /dev/null +++ b/papers/filebroker.py @@ -0,0 +1,98 @@ +import os + +def check_file(path, fail=True): + if fail: + if not os.path.exists(path): + raise IOError("File does not exist: {}.".format(path)) + if not os.path.isfile(path): + raise IOError("{} is not a file.".format(path)) + return True + else: + return os.path.exists(path) and os.path.isfile(path) + +def check_directory(path, fail=True): + if fail: + if not os.path.exists(path): + raise IOError("File does not exist: {}.".format(path)) + if not os.path.isdir(path): + raise IOError("{} is not a directory.".format(path)) + return True + else: + return os.path.exists(path) and os.path.isdir(path) + +def read_file(filepath): + check_file(filepath) + with open(filepath, 'r') as f: + s = f.read() + return s + +def write_file(filepath, data): + check_directory(os.path.dirname(filepath)) + with open(filepath, 'w') as f: + f.write(data) + + +class FileBroker(object): + """ Handles all access to meta and bib files of the repository. + + * Does *absolutely no* encoding/decoding. + * Communicate failure with exceptions. + """ + + def __init__(self, directory, create=False): + self.directory = directory + self.metadir = os.path.join(self.directory, 'meta') + self.bibdir = os.path.join(self.directory, 'bib') + if create: + self._create() + check_directory(self.directory) + check_directory(self.metadir) + check_directory(self.bibdir) + + def _create(self): + if not check_directory(self.directory, fail = False): + os.mkdir(self.directory) + if not check_directory(self.metadir, fail = False): + os.mkdir(self.metadir) + if not check_directory(self.bibdir, fail = False): + os.mkdir(self.bibdir) + + def pull_metafile(self, citekey): + filepath = os.path.join(self.metadir, citekey + '.yaml') + return read_file(filepath) + + def pull_bibfile(self, citekey): + filepath = os.path.join(self.bibdir, citekey + '.bibyaml') + return read_file(filepath) + + def push_metafile(self, citekey, metadata): + filepath = os.path.join(self.metadir, citekey + '.yaml') + write_file(filepath, metadata) + + def push_bibfile(self, citekey, bibdata): + filepath = os.path.join(self.bibdir, citekey + '.bibyaml') + write_file(filepath, bibdata) + + def push(self, citekey, metadata, bibdata): + self.push_metafile(citekey, metadata) + self.push_bibfile(citekey, bibdata) + + def remove(self, citekey): + metafilepath = os.path.join(self.metadir, citekey + '.yaml') + os.remove(metafilepath) + bibfilepath = os.path.join(self.bibdir, citekey + '.bibyaml') + os.remove(bibfilepath) + + def listing(self, filestats = True): + metafiles = [] + for filename in os.listdir(self.metadir): + stats = os.stat(os.path.join(path, f)) + metafiles.append(filename, stats) + + bibfiles = [] + for filename in os.listdir(self.bibdir): + stats = os.stat(os.path.join(path, f)) + bibfiles.append(filename, stats) + + return {'metafiles': metafiles, 'bibfiles': bibfiles} + diff --git a/tests/test_filebroker.py b/tests/test_filebroker.py new file mode 100644 index 0000000..4dbbea1 --- /dev/null +++ b/tests/test_filebroker.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import unittest +import os + +import testenv +import fake_env + +from papers import filebroker + +class TestFakeFs(unittest.TestCase): + """Abstract TestCase intializing the fake filesystem.""" + + def setUp(self): + self.fs = fake_env.create_fake_fs([filebroker]) + + def tearDown(self): + fake_env.unset_fake_fs([filebroker]) + + +class TestEnDecode(TestFakeFs): + + def test_pushpull1(self): + + fb = filebroker.FileBroker('bla', create = True) + + fb.push_metafile('citekey1', 'abc') + fb.push_bibfile('citekey1', 'cdef') + + self.assertEqual(fb.pull_metafile('citekey1'), 'abc') + self.assertEqual(fb.pull_bibfile('citekey1'), 'cdef') + + fb.push_bibfile('citekey1', 'ghi') + + self.assertEqual(fb.pull_bibfile('citekey1'), 'ghi') + + def test_existing_data(self): + + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'tmpdir'), 'tmpdir') + fb = filebroker.FileBroker('tmpdir', create = True) + + with open('tmpdir/bib/Page99.bibyaml', 'r') as f: + self.assertEqual(fb.pull_bibfile('Page99'), f.read()) + + with open('tmpdir/meta/Page99.yaml', 'r') as f: + self.assertEqual(fb.pull_metafile('Page99'), f.read()) From 15857b5eccb45beebe11d10335b535b8f535ce4a Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 9 Nov 2013 18:22:39 +0100 Subject: [PATCH 05/48] docbroker class + tests + more filebroker test --- papers/content.py | 87 ++++++++++++++++++++++++ papers/filebroker.py | 140 ++++++++++++++++++++++++++++----------- tests/test_filebroker.py | 75 +++++++++++++++++++-- 3 files changed, 260 insertions(+), 42 deletions(-) create mode 100644 papers/content.py diff --git a/papers/content.py b/papers/content.py new file mode 100644 index 0000000..96e080b --- /dev/null +++ b/papers/content.py @@ -0,0 +1,87 @@ +import os + + # files i/o + +def check_file(path, fail=True): + if fail: + if not os.path.exists(path): + raise IOError("File does not exist: {}.".format(path)) + if not os.path.isfile(path): + raise IOError("{} is not a file.".format(path)) + return True + else: + return os.path.exists(path) and os.path.isfile(path) + +def check_directory(path, fail=True): + if fail: + if not os.path.exists(path): + raise IOError("File does not exist: {}.".format(path)) + if not os.path.isdir(path): + raise IOError("{} is not a directory.".format(path)) + return True + else: + return os.path.exists(path) and os.path.isdir(path) + +def read_file(filepath): + check_file(filepath) + with open(filepath, 'r') as f: + s = f.read() + return s + +def write_file(filepath, data): + check_directory(os.path.dirname(filepath)) + with open(filepath, 'w') as f: + f.write(data) + + + # dealing with formatless content + +def get_content(self, path): + """Will be useful when we need to get content from url""" + return read_file(path) + +def move_content(self, source, target, overwrite = False): + if source == target: + return + if not overwrite and os.path.exists(target): + raise IOError('target file exists') + shutil.move(source, target) + +def copy_content(self, source, target, overwrite = False): + if source == target: + return + if not overwrite and os.path.exists(target): + raise IOError('target file exists') + shutil.copy(source, target) + + + # editor input + +def editor_input(editor, initial="", suffix=None): + """Use an editor to get input""" + if suffix is None: + suffix = '.tmp' + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: + tfile_name = temp_file.name + temp_file.write(initial) + temp_file.flush() + cmd = editor.split() # this enable editor command with option, e.g. gvim -f + cmd.append(tfile_name) + subprocess.call(cmd) + with open(tfile_name) as temp_file: + content = temp_file.read() + os.remove(tfile_name) + return content + +def edit_file(editor, path_to_file, temporary=True): + if temporary: + check_file(path_to_file, fail=True) + with open(path_to_file) as f: + content = f.read() + content = editor_input(editor, content) + with open(path_to_file, 'w') as f: + f.write(content) + else: + cmd = editor.split() # this enable editor command with option, e.g. gvim -f + cmd.append(path_to_file) + subprocess.call(cmd) diff --git a/papers/filebroker.py b/papers/filebroker.py index 0517772..faabcc4 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -1,36 +1,15 @@ import os +import urlparse -def check_file(path, fail=True): - if fail: - if not os.path.exists(path): - raise IOError("File does not exist: {}.".format(path)) - if not os.path.isfile(path): - raise IOError("{} is not a file.".format(path)) - return True - else: - return os.path.exists(path) and os.path.isfile(path) - -def check_directory(path, fail=True): - if fail: - if not os.path.exists(path): - raise IOError("File does not exist: {}.".format(path)) - if not os.path.isdir(path): - raise IOError("{} is not a directory.".format(path)) - return True - else: - return os.path.exists(path) and os.path.isdir(path) - -def read_file(filepath): - check_file(filepath) - with open(filepath, 'r') as f: - s = f.read() - return s - -def write_file(filepath, data): - check_directory(os.path.dirname(filepath)) - with open(filepath, 'w') as f: - f.write(data) +from .content import check_file, check_directory, read_file, write_file +def filter_filename(filename, ext): + """ Return the filename without the extension if the extension matches ext. + Otherwise return None + """ + pattern ='.*\{}$'.format(ext) + if re.match(pattern, filename) is not None: + return filename[:-len(ext)] class FileBroker(object): """ Handles all access to meta and bib files of the repository. @@ -66,33 +45,120 @@ class FileBroker(object): return read_file(filepath) def push_metafile(self, citekey, metadata): + """Put content to disk. Will gladly override anything standing in its way.""" filepath = os.path.join(self.metadir, citekey + '.yaml') write_file(filepath, metadata) def push_bibfile(self, citekey, bibdata): + """Put content to disk. Will gladly override anything standing in its way.""" filepath = os.path.join(self.bibdir, citekey + '.bibyaml') write_file(filepath, bibdata) def push(self, citekey, metadata, bibdata): + """Put content to disk. Will gladly override anything standing in its way.""" self.push_metafile(citekey, metadata) self.push_bibfile(citekey, bibdata) def remove(self, citekey): metafilepath = os.path.join(self.metadir, citekey + '.yaml') - os.remove(metafilepath) + if check_file(metafilepath): + os.remove(metafilepath) bibfilepath = os.path.join(self.bibdir, citekey + '.bibyaml') - os.remove(bibfilepath) + if check_file(bibfilepath): + os.remove(bibfilepath) + + def exists(self, citekey, both=True): + if both: + return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) and + check_file(os.path.join(self.bibdir, citekey + '.bibyaml'), fail=False)) + else: + return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) or + check_file(os.path.join(self.bibdir, citekey + '.bibyaml'), fail=False)) + - def listing(self, filestats = True): + def listing(self, filestats=True): metafiles = [] for filename in os.listdir(self.metadir): - stats = os.stat(os.path.join(path, f)) - metafiles.append(filename, stats) + citekey = filter_filename(filename, '.yaml') + if citekey is not None: + if filestats: + stats = os.stat(os.path.join(path, filename)) + metafiles.append(citekey, stats) + else: + metafiles.append(citekey) bibfiles = [] for filename in os.listdir(self.bibdir): - stats = os.stat(os.path.join(path, f)) - bibfiles.append(filename, stats) + citekey = filter_filename(filename, '.bibyaml') + if citekey is not None: + if filestats: + stats = os.stat(os.path.join(path, filename)) + bibfiles.append(citekey, stats) + else: + bibfiles.append(citekey) return {'metafiles': metafiles, 'bibfiles': bibfiles} + +class DocBroker(object): + """ DocBroker manages the document files optionally attached to the papers. + + * only one document can be attached to a paper (might change in the future) + * this document can be anything, the content is never processed. + * these document have an adress of the type "pubsdir://doc/citekey.pdf" + * document outside of the repository will not be removed. + * deliberately, there is no move_doc method. + """ + + def __init__(self, directory): + self.docdir = os.path.join(directory, 'doc') + if not check_directory(self.docdir, fail = False): + os.mkdir(self.docdir) + + def is_pubsdir_doc(self, docpath): + parsed = urlparse.urlparse(docpath) + if parsed.scheme == 'pubsdir': + assert parsed.netloc == 'doc' + assert parsed.path[0] == '/' + return parsed.scheme == 'pubsdir' + + def copy_doc(self, citekey, source_path, overwrite=False): + """ Copy a document to the pubsdir/doc, and return the location + + The document will be named {citekey}.{ext}. + The location will be pubsdir://doc/{citekey}.{ext}. + :param overwrite: will overwrite existing file. + :return: the above location + """ + full_source_path = self.real_docpath(source_path) + check_file(full_source_path) + + target_path = 'pubsdir://' + os.path.join('doc', citekey + os.path.splitext(source_path)[-1]) + full_target_path = self.real_docpath(target_path) + if not overwrite and check_file(full_target_path, fail=False): + raise IOError('{} file exists.'.format(full_target_path)) + shutil.copy(full_source_path, full_target_path) + + return target_path + + def remove_doc(self, docpath): + """ Will remove only file hosted in pubsdir://doc/ + + :raise ValueError: for other paths. + """ + if not self.is_pubsdir_doc(docpath): + raise ValueError(('the file to be removed {} is set as external. ' + 'you should remove it manually.').format(docpath)) + filepath = self.real_docpath(docpath) + if check_file(filepath): + os.remove(filepath) + + def real_docpath(self, docpath): + """Return the full path + Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}. + Return absoluted paths of regular ones otherwise. + """ + if self.is_pubsdir_doc(docpath): + parsed = urlparse.urlparse(docpath) + docpath = os.path.join(self.docdir, parsed.path[1:]) + return os.path.normpath(os.path.abspath(docpath)) diff --git a/tests/test_filebroker.py b/tests/test_filebroker.py index 4dbbea1..2691dad 100644 --- a/tests/test_filebroker.py +++ b/tests/test_filebroker.py @@ -5,19 +5,19 @@ import os import testenv import fake_env -from papers import filebroker +from papers import content, filebroker class TestFakeFs(unittest.TestCase): """Abstract TestCase intializing the fake filesystem.""" def setUp(self): - self.fs = fake_env.create_fake_fs([filebroker]) + self.fs = fake_env.create_fake_fs([content, filebroker]) def tearDown(self): - fake_env.unset_fake_fs([filebroker]) + fake_env.unset_fake_fs([content, filebroker]) -class TestEnDecode(TestFakeFs): +class TestFileBroker(TestFakeFs): def test_pushpull1(self): @@ -35,7 +35,7 @@ class TestEnDecode(TestFakeFs): def test_existing_data(self): - fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'tmpdir'), 'tmpdir') + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'tmpdir'), 'tmpdir') fb = filebroker.FileBroker('tmpdir', create = True) with open('tmpdir/bib/Page99.bibyaml', 'r') as f: @@ -43,3 +43,68 @@ class TestEnDecode(TestFakeFs): with open('tmpdir/meta/Page99.yaml', 'r') as f: self.assertEqual(fb.pull_metafile('Page99'), f.read()) + + def test_errors(self): + + with self.assertRaises(IOError): + filebroker.FileBroker('tmpdir', create = False) + + fb = filebroker.FileBroker('tmpdir', create = True) + with self.assertRaises(IOError): + fb.pull_bibfile('Page99') + with self.assertRaises(IOError): + fb.pull_metafile('Page99') + + def test_errors(self): + + with self.assertRaises(IOError): + filebroker.FileBroker('tmpdir', create = False) + + fb = filebroker.FileBroker('tmpdir', create = True) + + self.assertFalse(fb.exists('Page99')) + with self.assertRaises(IOError): + fb.pull_bibfile('Page99') + with self.assertRaises(IOError): + fb.pull_metafile('Page99') + + def test_remove(self): + + with self.assertRaises(IOError): + filebroker.FileBroker('tmpdir', create = False) + + fb = filebroker.FileBroker('tmpdir', create = True) + + fb.push_bibfile('citekey1', 'abc') + self.assertEqual(fb.pull_bibfile('citekey1'), 'abc') + fb.push_metafile('citekey1', 'defg') + self.assertEqual(fb.pull_metafile('citekey1'), 'defg') + self.assertTrue(fb.exists('citekey1')) + + fb.remove('citekey1') + with self.assertRaises(IOError): + self.assertEqual(fb.pull_bibfile('citekey1'), 'abc') + with self.assertRaises(IOError): + self.assertEqual(fb.pull_metafile('citekey1'), 'defg') + self.assertFalse(fb.exists('citekey1')) + + +class TestDocBroker(TestFakeFs): + + def test_doccopy(self): + + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'data'), 'data') + + fb = filebroker.FileBroker('tmpdir', create = True) + docb = filebroker.DocBroker('tmpdir') + + docpath = docb.copy_doc('Page99', 'data/pagerank.pdf') + self.assertTrue(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'))) + + self.assertTrue(docb.is_pubsdir_doc(docpath)) + self.assertEqual(docpath, 'pubsdir://doc/Page99.pdf') + docb.remove_doc('pubsdir://doc/Page99.pdf') + + self.assertFalse(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'), fail=False)) + with self.assertRaises(IOError): + self.assertFalse(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'), fail=True)) From a774a1604eb2494ed8122a310b478e58184bc37b Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sat, 9 Nov 2013 19:39:59 +0100 Subject: [PATCH 06/48] databroker, datacache class + tests --- papers/databroker.py | 65 ++++++++++++++ papers/datacache.py | 85 ++++++++++++++++++ tests/test_databroker.py | 69 ++++++++++++++ .../bib/10.1371_journal.pone.0038236.bibyaml | 45 ++++++++++ .../bib/10.1371journal.pone.0063400.bibyaml | 36 ++++++++ tests/testrepo/bib/Page99.bibyaml | 28 ++++++ tests/testrepo/bib/journal0063400.bibyaml | 15 ++++ tests/testrepo/doc/Page99.pdf | Bin 0 -> 306923 bytes .../meta/10.1371_journal.pone.0038236.yaml | 3 + .../meta/10.1371journal.pone.0063400.yaml | 3 + tests/testrepo/meta/Page99.yaml | 3 + tests/testrepo/meta/journal0063400.yaml | 3 + 12 files changed, 355 insertions(+) create mode 100644 papers/databroker.py create mode 100644 papers/datacache.py create mode 100644 tests/test_databroker.py create mode 100644 tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml create mode 100644 tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml create mode 100644 tests/testrepo/bib/Page99.bibyaml create mode 100644 tests/testrepo/bib/journal0063400.bibyaml create mode 100644 tests/testrepo/doc/Page99.pdf create mode 100644 tests/testrepo/meta/10.1371_journal.pone.0038236.yaml create mode 100644 tests/testrepo/meta/10.1371journal.pone.0063400.yaml create mode 100644 tests/testrepo/meta/Page99.yaml create mode 100644 tests/testrepo/meta/journal0063400.yaml diff --git a/papers/databroker.py b/papers/databroker.py new file mode 100644 index 0000000..fed7307 --- /dev/null +++ b/papers/databroker.py @@ -0,0 +1,65 @@ +from . import filebroker +from . import endecoder + + +class DataBroker(object): + """ DataBroker class + + This is aimed at being a simple, high level interface to the content stored on disk. + Requests are optimistically made, and exceptions are raised if something goes wrong. + """ + + def __init__(self, directory, create=False): + self.filebroker = filebroker.FileBroker(directory, create=create) + self.endecoder = endecoder.EnDecoder() + self.docbroker = filebroker.DocBroker(directory) + + # filebroker+endecoder + + def pull_metadata(self, citekey): + metadata_raw = self.filebroker.pull_metafile(citekey) + return self.endecoder.decode_metadata(metadata_raw) + + def pull_bibdata(self, citekey): + bibdata_raw = self.filebroker.pull_bibfile(citekey) + return self.endecoder.decode_bibdata(bibdata_raw) + + def push_metadata(self, citekey, metadata): + metadata_raw = self.endecoder.encode_metadata(metadata) + self.filebroker.push_metafile(citekey, metadata_raw) + + def push_bibdata(self, citekey, bibdata): + bibdata_raw = self.endecoder.encode_bibdata(bibdata) + self.filebroker.push_bibfile(citekey, bibdata_raw) + + def push(self, citekey, metadata, bibdata): + self.filebroker.push(citekey, metadata, bibdata) + + def remove(self, citekey): + self.filebroker.remove(citekey) + + def exists(self, citekey, both = True): + return self.filebroker.exists(citekey, both=both) + + def listing(self, filestats=True): + return self.filebroker.listing(filestats=filestats) + + def verify(self, bibdata_raw): + try: + return self.endecoder.decode_bibdata(bibdata_raw) + except ValueError: + return None + + # docbroker + + def is_pubsdir_doc(self, docpath): + return self.docbroker.is_pusdir_doc(docpath) + + def copy_doc(self, citekey, source_path, overwrite=False): + return self.docbroker.copy_doc(citekey, source_path, overwrite=overwrite) + + def remove_doc(self, docpath): + return self.docbroker.remove_doc(docpath) + + def real_docpath(self, docpath): + return self.docbroker.real_docpath(docpath) \ No newline at end of file diff --git a/papers/datacache.py b/papers/datacache.py new file mode 100644 index 0000000..0d75fb3 --- /dev/null +++ b/papers/datacache.py @@ -0,0 +1,85 @@ + +from . import databroker + +class DataCache(object): + """ DataCache class, provides a very similar interface as DataBroker + + Has two roles : + 1. Provides a buffer between the commands and the hard drive. + Until a command request a hard drive ressource, it does not touch it. + 2. Keeps a up-to-date, pickled version of the repository, to speed up things + when they are a lot of files. Update are also done only when required. + Changes are detected using data modification timestamps. + + For the moment, only (1) is implemented. + """ + def __init__(self, directory, create=False): + self.directory = directory + self._databroker = None + if create: + self._create() + + @property + def databroker(self): + if self._databroker is None: + self._databroker = databroker.DataBroker(self.directory, create=False) + return self._databroker + + def _create(self): + self._databroker = databroker.DataBroker(self.directory, create=True) + + def pull_metadata(self, citekey): + return self.databroker.pull_metadata(citekey) + + def pull_bibdata(self, citekey): + return self.databroker.pull_bibdata(citekey) + + def push_metadata(self, citekey, metadata): + self.databroker.push_metadata(citekey, metadata) + + def push_bibdata(self, citekey, bibdata): + self.databroker.push_bibdata(citekey, bibdata) + + def push(self, citekey, metadata, bibdata): + self.databroker.push(citekey, metadata, bibdata) + + def remove(self, citekey): + self.databroker.remove(citekey) + + def exists(self, citekey, both=True): + self.databroker.exists(citekey, both=both) + + def citekeys(self): + listings = self.listing(filestats=False) + return set(listings['metafiles']).intersection(listings['bibfiles']) + + def listing(self, filestats=True): + return self.databroker.listing(filestats=filestats) + + def verify(self, bibdata_raw): + """Will return None if bibdata_raw can't be decoded""" + return self.databroker.verify(bibdata_raw) + + # docbroker + + def is_pubsdir_doc(self, docpath): + return self.databroker.is_pusdir_doc(docpath) + + def copy_doc(self, citekey, source_path, overwrite=False): + return self.databroker.copy_doc(citekey, source_path, overwrite=overwrite) + + def remove_doc(self, docpath): + return self.databroker.remove_doc(docpath) + + def real_docpath(self, docpath): + return self.databroker.real_docpath(docpath) + +# class ChangeTracker(object): + +# def __init__(self, cache, directory): +# self.cache = cache +# self.directory = directory + +# def changes(self): +# """ Returns the list of modified files since the last cache was saved to disk""" +# pass diff --git a/tests/test_databroker.py b/tests/test_databroker.py new file mode 100644 index 0000000..8cb6344 --- /dev/null +++ b/tests/test_databroker.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +import unittest +import os + +import testenv +import fake_env + +from papers import content, filebroker, databroker, datacache + +import str_fixtures +from papers import endecoder + +class TestFakeFs(unittest.TestCase): + """Abstract TestCase intializing the fake filesystem.""" + + def setUp(self): + self.fs = fake_env.create_fake_fs([content, filebroker]) + + def tearDown(self): + fake_env.unset_fake_fs([content, filebroker]) + + +class TestDataBroker(TestFakeFs): + + def test_databroker(self): + + ende = endecoder.EnDecoder() + page99_metadata = ende.decode_metadata(str_fixtures.metadata_raw0) + page99_bibdata = ende.decode_bibdata(str_fixtures.bibyaml_raw0) + + dtb = databroker.DataBroker('tmp', create=True) + dtc = datacache.DataCache('tmp') + + for db in [dtb, dtc]: + db.push_metadata('citekey1', page99_metadata) + db.push_bibdata('citekey1', page99_bibdata) + + self.assertEqual(db.pull_metadata('citekey1'), page99_metadata) + self.assertEqual(db.pull_bibdata('citekey1'), page99_bibdata) + + def test_existing_data(self): + + ende = endecoder.EnDecoder() + page99_bibdata = ende.decode_bibdata(str_fixtures.bibyaml_raw0) + + for db_class in [databroker.DataBroker, datacache.DataCache]: + self.fs = fake_env.create_fake_fs([content, filebroker]) + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'testrepo'), 'repo') + + db = db_class('repo', create=False) + + self.assertEqual(db.pull_bibdata('Page99'), page99_bibdata) + + for citekey in ['10.1371_journal.pone.0038236', + '10.1371journal.pone.0063400', + 'journal0063400']: + db.pull_bibdata(citekey) + db.pull_metadata(citekey) + + with self.assertRaises(IOError): + db.pull_bibdata('citekey') + with self.assertRaises(IOError): + db.pull_metadata('citekey') + + db.copy_doc('Larry99', 'pubsdir://doc/Page99.pdf') + self.assertTrue(content.check_file('repo/doc/Page99.pdf', fail=False)) + self.assertTrue(content.check_file('repo/doc/Larry99.pdf', fail=False)) + + db.remove_doc('pubsdir://doc/Page99.pdf') diff --git a/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml b/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml new file mode 100644 index 0000000..26da434 --- /dev/null +++ b/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml @@ -0,0 +1,45 @@ +entries: + 10.1371_journal.pone.0038236: + abstract:

The advent of humanoid robots has enabled a new approach to investigating + the acquisition of language, and we report on the development of robots + able to acquire rudimentary linguistic skills. Our work focuses on early + stages analogous to some characteristics of a human child of about 6 to + 14 months, the transition from babbling to first word forms. We investigate + one mechanism among many that may contribute to this process, a key factor + being the sensitivity of learners to the statistical distribution of linguistic + elements. As well as being necessary for learning word meanings, the acquisition + of anchor word forms facilitates the segmentation of an acoustic stream + through other mechanisms. In our experiments some salient one-syllable + word forms are learnt by a humanoid robot in real-time interactions with + naive participants. Words emerge from random syllabic babble through a + learning process based on a dialogue between the robot and the human participant, + whose speech is perceived by the robot as a stream of phonemes. Numerous + ways of representing the speech as syllabic segments are possible. Furthermore, + the pronunciation of many words in spontaneous speech is variable. However, + in line with research elsewhere, we observe that salient content words + are more likely than function words to have consistent canonical representations; + thus their relative frequency increases, as does their influence on the + learner. Variable pronunciation may contribute to early word form acquisition. + The importance of contingent interaction in real-time between teacher + and learner is reflected by a reinforcement process, with variable success. + The examination of individual cases may be more informative than group + results. Nevertheless, word forms are usually produced by the robot after + a few minutes of dialogue, employing a simple, real-time, frequency dependent + mechanism. This work shows the potential of human-robot interaction systems + in studies of the dynamics of early language acquisition.

+ author: + - first: Caroline + last: Saunders + middle: Lyon AND Chrystopher L. Nehaniv AND Joe + doi: 10.1371/journal.pone.0038236 + journal: PLoS ONE + month: '06' + number: '6' + pages: e38236 + publisher: Public Library of Science + title: 'Interactive Language Learning by Robots: The Transition from Babbling + to Word Forms' + type: article + url: http://dx.doi.org/10.1371%2Fjournal.pone.0038236 + volume: '7' + year: '2012' diff --git a/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml b/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml new file mode 100644 index 0000000..bdfda50 --- /dev/null +++ b/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml @@ -0,0 +1,36 @@ +entries: + 10.1371journal.pone.0063400: + abstract:

Information theory is a powerful tool to express principles to + drive autonomous systems because it is domain invariant and allows for + an intuitive interpretation. This paper studies the use of the predictive + information (PI), also called excess entropy or effective measure complexity, + of the sensorimotor process as a driving force to generate behavior. We + study nonlinear and nonstationary systems and introduce the time-local + predicting information (TiPI) which allows us to derive exact results + together with explicit update rules for the parameters of the controller + in the dynamical systems framework. In this way the information principle, + formulated at the level of behavior, is translated to the dynamics of + the synapses. We underpin our results with a number of case studies with + high-dimensional robotic systems. We show the spontaneous cooperativity + in a complex physical system with decentralized control. Moreover, a jointly + controlled humanoid robot develops a high behavioral variety depending + on its physics and the environment it is dynamically embedded into. The + behavior can be decomposed into a succession of low-dimensional modes + that increasingly explore the behavior space. This is a promising way + to avoid the curse of dimensionality which hinders learning systems to + scale well.

+ author: + - first: Georg + last: Ay + middle: Martius AND Ralf Der AND Nihat + doi: 10.1371/journal.pone.0063400 + journal: PLoS ONE + month: '05' + number: '5' + pages: e63400 + publisher: Public Library of Science + title: Information Driven Self-Organization of Complex Robotic Behaviors + type: article + url: http://dx.doi.org/10.1371%2Fjournal.pone.0063400 + volume: '8' + year: '2013' diff --git a/tests/testrepo/bib/Page99.bibyaml b/tests/testrepo/bib/Page99.bibyaml new file mode 100644 index 0000000..3e77c1c --- /dev/null +++ b/tests/testrepo/bib/Page99.bibyaml @@ -0,0 +1,28 @@ +entries: + Page99: + abstract: The importance of a Web page is an inherently subjective matter, + which depends on the readers interests, knowledge and attitudes. But there + is still much that can be said objectively about the relative importance + of Web pages. This paper describes PageRank, a mathod for rating Web pages + objectively and mechanically, effectively measuring the human interest + and attention devoted to them. We compare PageRank to an idealized random + Web surfer. We show how to efficiently compute PageRank for large numbers + of pages. And, we show how to apply PageRank to search and to user navigation. + author: + - first: Lawrence + last: Page + - first: Sergey + last: Brin + - first: Rajeev + last: Motwani + - first: Terry + last: Winograd + institution: Stanford InfoLab + month: November + note: Previous number = SIDL-WP-1999-0120 + number: 1999-66 + publisher: Stanford InfoLab + title: 'The PageRank Citation Ranking: Bringing Order to the Web.' + type: techreport + url: http://ilpubs.stanford.edu:8090/422/ + year: '1999' diff --git a/tests/testrepo/bib/journal0063400.bibyaml b/tests/testrepo/bib/journal0063400.bibyaml new file mode 100644 index 0000000..041a029 --- /dev/null +++ b/tests/testrepo/bib/journal0063400.bibyaml @@ -0,0 +1,15 @@ +entries: + journal0063400: + author: + - first: Lawrence + last: Page + - first: Sergey + last: Brin + - first: Rajeev + last: Motwani + - first: Terry + last: Winograd + journal: PLoS ONE + publisher: Public Library of Science + title: Information Driven Self-Organization of Complex Robotic Behaviors + type: article diff --git a/tests/testrepo/doc/Page99.pdf b/tests/testrepo/doc/Page99.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0523ae00b2f93dc425edda1ef1dd852af7a044fe GIT binary patch literal 306923 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

pQL6N7r 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 z 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 z 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

O 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 literal 0 HcmV?d00001 diff --git a/tests/testrepo/meta/10.1371_journal.pone.0038236.yaml b/tests/testrepo/meta/10.1371_journal.pone.0038236.yaml new file mode 100644 index 0000000..21ea181 --- /dev/null +++ b/tests/testrepo/meta/10.1371_journal.pone.0038236.yaml @@ -0,0 +1,3 @@ +docfile: null +notes: [] +tags: [] diff --git a/tests/testrepo/meta/10.1371journal.pone.0063400.yaml b/tests/testrepo/meta/10.1371journal.pone.0063400.yaml new file mode 100644 index 0000000..21ea181 --- /dev/null +++ b/tests/testrepo/meta/10.1371journal.pone.0063400.yaml @@ -0,0 +1,3 @@ +docfile: null +notes: [] +tags: [] diff --git a/tests/testrepo/meta/Page99.yaml b/tests/testrepo/meta/Page99.yaml new file mode 100644 index 0000000..44a426c --- /dev/null +++ b/tests/testrepo/meta/Page99.yaml @@ -0,0 +1,3 @@ +docfile: pubsdir://doc/Page99.pdf +notes: [] +tags: [search, network] diff --git a/tests/testrepo/meta/journal0063400.yaml b/tests/testrepo/meta/journal0063400.yaml new file mode 100644 index 0000000..21ea181 --- /dev/null +++ b/tests/testrepo/meta/journal0063400.yaml @@ -0,0 +1,3 @@ +docfile: null +notes: [] +tags: [] From 6190890646b25d37e001172f2c60946e010eaba9 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 00:38:06 +0100 Subject: [PATCH 07/48] updated init cmd, configs + bumped to version 4 --- papers/__init__.py | 2 +- papers/commands/__init__.py | 22 +++++++++++----------- papers/commands/init_cmd.py | 36 ++++++++++++++++++++---------------- papers/configs.py | 26 ++++++++++++++------------ papers/papers_cmd.py | 22 +++++++++++----------- setup.py | 2 +- 6 files changed, 58 insertions(+), 52 deletions(-) diff --git a/papers/__init__.py b/papers/__init__.py index fe95ae2..b081b7c 100644 --- a/papers/__init__.py +++ b/papers/__init__.py @@ -1 +1 @@ -__version__ = 3 \ No newline at end of file +__version__ = 4 \ No newline at end of file diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index 466e2ea..03603a9 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -1,12 +1,12 @@ -import add_cmd -import import_cmd -import export_cmd import init_cmd -import list_cmd -import open_cmd -import edit_cmd -import remove_cmd -import websearch_cmd -import tag_cmd -import attach_cmd -import update_cmd +# import add_cmd +# import import_cmd +# import export_cmd +# import list_cmd +# import edit_cmd +# import remove_cmd +# import open_cmd +# import websearch_cmd +# import tag_cmd +# import attach_cmd +# import update_cmd diff --git a/papers/commands/init_cmd.py b/papers/commands/init_cmd.py index cfdb177..622581d 100644 --- a/papers/commands/init_cmd.py +++ b/papers/commands/init_cmd.py @@ -2,20 +2,20 @@ import os -from ..repo import Repository +from .. import databroker from ..configs import config from ..uis import get_ui from .. import color -from .. import files + def parser(subparsers): parser = subparsers.add_parser('init', help="initialize the papers directory") - parser.add_argument('-p', '--path', default=None, + parser.add_argument('-p', '--pubsdir', default=None, help='path to papers directory (if none, ~/.papers is used)') - parser.add_argument('-d', '--doc-dir', default=None, - help=('path to document directory (if none, documents ' - 'are stored in the same directory)')) + parser.add_argument('-d', '--docsdir', default='docsdir://', + help=('path to document directory (if not specified, documents will' + 'be stored in /path/to/pubsdir/doc/)')) return parser @@ -23,20 +23,24 @@ def command(args): """Create a .papers directory""" ui = get_ui() - path = args.path - doc_dir = args.doc_dir + pubsdir = args.pubsdir + docsdir = args.docsdir + + if pubsdir is None: + pubsdir = '~/.papers' + + pubsdir = os.path.normpath(os.path.abspath(os.path.expanduser(pubsdir))) - if path is not None: - config().papers_dir = files.clean_path(os.getcwd(), path) - ppd = config().papers_dir - if os.path.exists(ppd) and len(os.listdir(ppd)) > 0: + if os.path.exists(pubsdir) and len(os.listdir(pubsdir)) > 0: ui.error('directory {} is not empty.'.format( - color.dye(ppd, color.filepath))) + color.dye(pubsdir, color.filepath))) ui.exit() ui.print_('Initializing papers in {}.'.format( - color.dye(ppd, color.filepath))) + color.dye(pubsdir, color.filepath))) - repo = Repository(config(), load = False) - repo.save() + config().pubsdir = pubsdir + config().docsdir = docsdir config().save() + + databroker.DataBroker(pubsdir, create=True) diff --git a/papers/configs.py b/papers/configs.py index ff48f0c..c538154 100644 --- a/papers/configs.py +++ b/papers/configs.py @@ -1,4 +1,6 @@ import os +import collections + from .p3 import configparser # constant stuff (DFT = DEFAULT) @@ -12,18 +14,18 @@ except KeyError: DFT_PLUGINS = '' -DFT_CONFIG = {'papers_dir' : os.path.expanduser('~/.papers'), - 'doc_dir' : 'doc', - 'import_copy' : True, - 'import_move' : False, - 'color' : True, - 'version' : 3, - 'version_warning' : True, - - 'open_cmd' : 'open', - 'edit_cmd' : DFT_EDIT_CMD, - 'plugins' : DFT_PLUGINS - } +DFT_CONFIG = collections.OrderedDict([ + ('pubsdir', os.path.expanduser('~/.papers')), + ('docsdir', ''), + ('import_copy', True), + ('import_move', False), + ('color', True), + ('version', 4), + ('version_warning', True), + ('open_cmd', 'open'), + ('edit_cmd', DFT_EDIT_CMD), + ('plugins', DFT_PLUGINS) + ]) BOOLEANS = {'import_copy', 'import_move', 'color', 'version_warning'} diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 5a4041e..dfd93e9 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -14,17 +14,17 @@ from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), - ('add', commands.add_cmd), - ('import', commands.import_cmd), - ('export', commands.export_cmd), - ('list', commands.list_cmd), - ('edit', commands.edit_cmd), - ('remove', commands.remove_cmd), - ('open', commands.open_cmd), - ('websearch', commands.websearch_cmd), - ('tag', commands.tag_cmd), - ('attach', commands.attach_cmd), - ('update', commands.update_cmd), + # ('add', commands.add_cmd), + # ('import', commands.import_cmd), + # ('export', commands.export_cmd), + # ('list', commands.list_cmd), + # ('edit', commands.edit_cmd), + # ('remove', commands.remove_cmd), + # ('open', commands.open_cmd), + # ('websearch', commands.websearch_cmd), + # ('tag', commands.tag_cmd), + # ('attach', commands.attach_cmd), + # ('update', commands.update_cmd), ]) diff --git a/setup.py b/setup.py index 952f549..05fd26f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='papers', - version='3', + version='4', author='Fabien Benureau, Olivier Mangin, Jonathan Grizou', author_email='fabien.benureau+inria@gmail.com', url='', From 1f6e25d915af7cfa7f93988a645a36ae5b11bb61 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:02:00 +0100 Subject: [PATCH 08/48] fix filebroker and contents bugs --- papers/content.py | 8 +++++--- papers/filebroker.py | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/papers/content.py b/papers/content.py index 96e080b..737653b 100644 --- a/papers/content.py +++ b/papers/content.py @@ -1,4 +1,6 @@ import os +import subprocess +import tempfile # files i/o @@ -36,18 +38,18 @@ def write_file(filepath, data): # dealing with formatless content -def get_content(self, path): +def get_content(path): """Will be useful when we need to get content from url""" return read_file(path) -def move_content(self, source, target, overwrite = False): +def move_content(source, target, overwrite = False): if source == target: return if not overwrite and os.path.exists(target): raise IOError('target file exists') shutil.move(source, target) -def copy_content(self, source, target, overwrite = False): +def copy_content(source, target, overwrite = False): if source == target: return if not overwrite and os.path.exists(target): diff --git a/papers/filebroker.py b/papers/filebroker.py index faabcc4..18a7452 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -1,4 +1,6 @@ import os +import shutil +import re import urlparse from .content import check_file, check_directory, read_file, write_file From d1a4dd584f98d055f6c32517451fd0bc8a28d0e1 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:02:34 +0100 Subject: [PATCH 09/48] bibstruct modules --- papers/bibstruct.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 papers/bibstruct.py diff --git a/papers/bibstruct.py b/papers/bibstruct.py new file mode 100644 index 0000000..8b9f317 --- /dev/null +++ b/papers/bibstruct.py @@ -0,0 +1,82 @@ +import unicodedata +import re + + # citekey stuff + +CONTROL_CHARS = ''.join(map(unichr, range(0, 32) + range(127, 160))) +CITEKEY_FORBIDDEN_CHARS = '@\'\\,#}{~%/' # '/' is OK for bibtex but forbidden +# here since we transform citekeys into filenames +CITEKEY_EXCLUDE_RE = re.compile('[%s]' + % re.escape(CONTROL_CHARS + CITEKEY_FORBIDDEN_CHARS)) + +def str2citekey(s): + key = unicodedata.normalize('NFKD', unicode(s)).encode('ascii', 'ignore') + key = CITEKEY_EXCLUDE_RE.sub('', key) + # Normalize chars and remove non-ascii + return key + +def check_citekey(citekey): + # TODO This is not the right way to test that (17/12/2012) + if unicode(citekey) != str2citekey(citekey): + raise ValueError("Invalid citekey: %s" % citekey) + +def verify_bibdata(bibdata): + if not hasattr(bibdata, 'entries') or len(bibdata.entries) == 0: + raise ValueError('no entries in the bibdata.') + if len(bibdata.entries) > 1: + raise ValueError('ambiguous: multiple entries in the bibdata.') + +def get_entry(bibdata): + verify_bibdata(bibdata) + return bibdata.entries.iteritems().next() + +def extract_citekey(bibdata): + verify_bibdata(bibdata) + citekey, entry = get_entry(bibdata) + return citekey + +def generate_citekey(bibdata): + """ Generate a citekey from bib_data. + + :param generate: if False, return the citekey defined in the file, + does not generate a new one. + :raise ValueError: if no author nor editor is defined. + """ + citekey, entry = get_entry(bibdata) + + author_key = 'author' if 'author' in entry.persons else 'editor' + try: + first_author = self.bibentry.persons[author_key][0] + except KeyError: + raise ValueError( + 'No author or editor defined: cannot generate a citekey.') + try: + year = self.bibentry.fields['year'] + except KeyError: + year = '' + citekey = u'{}{}'.format(u''.join(first_author.last()), year) + + return str2citekey(citekey) + +def extract_docfile(bibdata, remove=False): + """ Try extracting document file from bib data. + Returns None if not found. + + :param remove: remove field after extracting information (default: False) + """ + citekey, entry = get_entry(bibdata) + + try: + field = entry.fields['file'] + # Check if this is mendeley specific + for f in field.split(':'): + if len(f) > 0: + break + if remove: + entry.fields.pop('file') + # This is a hck for Mendeley. Make clean + if f[0] != '/': + f = '/' + f + return f + except (KeyError, IndexError): + return None From 161be4f994a873ab8ca4eb9e44ec5b40473b4280 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:03:03 +0100 Subject: [PATCH 10/48] Paper class --- papers/paper.py | 308 +++++------------------------------------------- 1 file changed, 32 insertions(+), 276 deletions(-) diff --git a/papers/paper.py b/papers/paper.py index 28b41c3..19a3dbd 100644 --- a/papers/paper.py +++ b/papers/paper.py @@ -1,260 +1,55 @@ -import os +import copy +import collections -import unicodedata -import re -from cStringIO import StringIO -import yaml - -from pybtex.database import Entry, BibliographyData, FieldDict, Person - -import files - - -DEFAULT_TYPE = 'article' - -CONTROL_CHARS = ''.join(map(unichr, range(0, 32) + range(127, 160))) -CITEKEY_FORBIDDEN_CHARS = '@\'\\,#}{~%/' # '/' is OK for bibtex but forbidden -# here since we transform citekeys into filenames -CITEKEY_EXCLUDE_RE = re.compile('[%s]' - % re.escape(CONTROL_CHARS + CITEKEY_FORBIDDEN_CHARS)) - -BASE_META = { - 'external-document': None, - 'tags': set(), - 'notes': [], - } - - -def str2citekey(s): - key = unicodedata.normalize('NFKD', unicode(s)).encode('ascii', 'ignore') - key = CITEKEY_EXCLUDE_RE.sub('', key) - # Normalize chars and remove non-ascii - return key - - -def get_bibentry_from_file(bibfile): - """Extract first entry (supposed to be the only one) from given file. - """ - bib_data = files.load_externalbibfile(bibfile) - first_key = list(bib_data.entries.keys())[0] - first_entry = bib_data.entries[first_key] - return first_key, first_entry - - -def get_bibentry_from_string(content): - """Extract first entry (supposed to be the only one) from given file. - """ - bib_data = files.parse_bibdata(StringIO(content)) - first_key = list(bib_data.entries.keys())[0] - first_entry = bib_data.entries[first_key] - return first_key, first_entry - - -def copy_person(p): - return Person(first=p.get_part_as_text('first'), - middle=p.get_part_as_text('middle'), - prelast=p.get_part_as_text('prelast'), - last=p.get_part_as_text('last'), - lineage=p.get_part_as_text('lineage')) - - -def copy_bibentry(entry): - fd = FieldDict(entry.fields.parent, entry.fields) - persons = dict([(k, [copy_person(p) for p in v]) - for k, v in entry.persons.items()]) - return Entry(entry.type, fields=fd, persons=persons) - - -def get_safe_metadata(meta): - base_meta = Paper.create_meta() - if meta is not None: - base_meta.update(meta) - base_meta['tags'] = set(base_meta['tags']) - return base_meta - - -def get_safe_metadata_from_content(content): - return get_safe_metadata(yaml.load(content)) - - -def get_safe_metadata_from_path(metapath): - if metapath is None: - content = None - else: - content = files.read_yamlfile(metapath) - return get_safe_metadata(content) - - -def check_citekey(citekey): - # TODO This is not the right way to test that (17/12/2012) - if unicode(citekey) != str2citekey(citekey): - raise ValueError("Invalid citekey: %s" % citekey) - - -class NoDocumentFile(Exception): - pass +from . import bibstruct +DEFAULT_META = collections.OrderedDict([('docfile', None), ('tags', set()), ('notes', [])]) +DEFAULT_META = {'docfile': None, 'tags': set(), 'notes': []} class Paper(object): - """Paper class. The object is responsible for the integrity of its own - data, and for loading and writing it to disc. + """ Paper class. The object is responsible for the integrity of its data - The object uses a pybtex.database.BibliographyData object to store - biblography data and an additional dictionary to store meta data. + The object is not responsible of any disk i/o. + self.bibdata is a pybtex.database.BibliographyData object + self.metadata is a dictionary """ - def __init__(self, bibentry=None, metadata=None, citekey=None): - if bibentry is None: - bibentry = Entry(DEFAULT_TYPE) - self.bibentry = bibentry - if metadata is None: - metadata = Paper.create_meta() + def __init__(self, bibdata, citekey=None, metadata=None): + self.citekey = citekey self.metadata = metadata - check_citekey(citekey) - self.citekey = citekey + self.bibdata = bibdata + + if self.metadata is None: + self.metadata = copy.deepcopy(DEFAULT_META) + if self.citekey is None: + self.citekey = bibstruct.extract_citekey(self.bibdata) + bibstruct.check_citekey(self.citekey) def __eq__(self, other): return (isinstance(self, Paper) and type(other) is type(self) - and self.bibentry == other.bibentry + and self.bibdata == other.bibdata and self.metadata == other.metadata - and self.citekey == other.citekey) + and self.citekey == other.citekey) def __repr__(self): return 'Paper(%s, %s, %s)' % ( self.citekey, self.bibentry, self.metadata) - def __str__(self): - return self.__repr__() - - # TODO add mechanism to verify keys (15/12/2012) - - def get_external_document_path(self): - if self.metadata['external-document'] is not None: - return self.metadata['external-document'] - else: - raise NoDocumentFile - - def get_document_path(self): - return self.get_external_document_path() - - def set_external_document(self, docpath): - fullpdfpath = os.path.abspath(docpath) - files.check_file(fullpdfpath, fail=True) - self.metadata['external-document'] = fullpdfpath - - def check_document_path(self): - return files.check_file(self.get_external_document_path()) - - def generate_citekey(self): - """Generate a citekey from bib_data. - - Raises: - KeyError if no author nor editor is defined. - """ - author_key = 'author' - if not 'author' in self.bibentry.persons: - author_key = 'editor' - try: - first_author = self.bibentry.persons[author_key][0] - except KeyError: - raise ValueError( - 'No author or editor defined: cannot generate a citekey.') - try: - year = self.bibentry.fields['year'] - except KeyError: - year = '' - citekey = u'{}{}'.format(u''.join(first_author.last()), year) - return str2citekey(citekey) - - def save(self, bib_filepath, meta_filepath): - """Creates a BibliographyData object containing a single entry and - saves it to disc. - """ - if self.citekey is None: - raise ValueError( - 'No valid citekey initialized. Cannot save paper') - bibdata = BibliographyData(entries={self.citekey: self.bibentry}) - files.save_bibdata(bibdata, bib_filepath) - files.save_meta(self.metadata, meta_filepath) - - def update(self, key=None, bib=None, meta=None): - if key is not None: - check_citekey(key) - self.citekey = key - if bib is not None: - self.bibentry = bib - if meta is not None: - self.metadata = meta - - def get_document_file_from_bibdata(self, remove=False): - """Try extracting document file from bib data. - Raises NoDocumentFile if not found. - - Parameters: - ----------- - remove: default: False - remove field after extracting information - """ - try: - field = self.bibentry.fields['file'] - # Check if this is mendeley specific - for f in field.split(':'): - if len(f) > 0: - break - if remove: - self.bibentry.fields.pop('file') - # This is a hck for Mendeley. Make clean - if f[0] != '/': - f = '/' + f - return f - except (KeyError, IndexError): - raise NoDocumentFile('No file found in bib data.') - - def copy(self): - return Paper(bibentry=copy_bibentry(self.bibentry), - metadata=self.metadata.copy(), - citekey=self.citekey) + def deepcopy(self): + return Paper(citekey =self.citekey, + metadata=copy.deepcopy(self.metadata), + bibdata=copy.deepcopy(self.bibdata)) - @classmethod - def load(cls, bibpath, metapath=None): - key, entry = get_bibentry_from_file(bibpath) - metadata = get_safe_metadata_from_path(metapath) - p = Paper(bibentry=entry, metadata=metadata, citekey=key) - return p - - @classmethod - def create_meta(cls): - return BASE_META.copy() - - @classmethod - def many_from_path(cls, bibpath): - """Extract list of papers found in bibliographic files in path. - - The behavior is to: - - ignore wrong entries, - - overwrite duplicated entries. - :returns: dictionary of (key, paper | exception) - if loading of entry failed, the excpetion is returned in the - dictionary in place of the paper - """ - bibpath = files.clean_path(bibpath) - if os.path.isdir(bibpath): - all_files = [os.path.join(bibpath, f) for f in os.listdir(bibpath) - if os.path.splitext(f)[-1] in files.BIB_EXTENSIONS] - else: - all_files = [bibpath] - bib_data = [files.load_externalbibfile(f) for f in all_files] - papers = {} - for b in bib_data: - for k in b.entries: - try: - papers[k] = Paper(bibentry=b.entries[k], citekey=k) - except ValueError, e: - papers[k] = e - return papers + @property + def docpath(self): + return self.metadata.get('docfile', '') + @docpath.setter + def docpath(self, path): + """Does not verify if the path exists.""" + self.metadata['docfile'] = path - # tags + # tags @property def tags(self): @@ -272,42 +67,3 @@ class Paper(object): def remove_tag(self, tag): """Remove a tag from a paper if present.""" self.tags.discard(tag) - - -class PaperInRepo(Paper): - """Extend paper class with command specific to the case where the paper - lives in a repository. - """ - - def __init__(self, repo, *args, **kwargs): - Paper.__init__(self, *args, **kwargs) - self.repo = repo - - def get_document_path_in_repo(self): - return self.repo.find_document(self.citekey) - - def get_document_path(self): - try: - return self.get_document_path_in_repo() - except NoDocumentFile: - return self.get_external_document_path() - - def copy(self): - return PaperInRepo.from_paper(self.as_paper().copy(), self.repo) - - def as_paper(self): - return Paper(bibentry=self.bibentry, - metadata=self.metadata, - citekey=self.citekey) - - @classmethod - def load(cls, repo, bibpath, metapath=None): - key, entry = get_bibentry_from_file(bibpath) - metadata = get_safe_metadata_from_path(metapath) - p = cls(repo, bibentry=entry, metadata=metadata, citekey=key) - return p - - @classmethod - def from_paper(cls, paper, repo): - return cls(repo, bibentry=paper.bibentry, metadata=paper.metadata, - citekey=paper.citekey) From c0480846640718986014a9ede470acef0c695537 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:03:32 +0100 Subject: [PATCH 11/48] Repository class --- papers/repo.py | 259 ++++++++++++++++--------------------------------- 1 file changed, 84 insertions(+), 175 deletions(-) diff --git a/papers/repo.py b/papers/repo.py index d721322..113be51 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -1,16 +1,14 @@ -import os import shutil import glob import itertools -from . import files -from .paper import PaperInRepo, NoDocumentFile, check_citekey -from .events import RemoveEvent, RenameEvent, AddEvent +from . import bibstruct +from . import events +from . import datacache +from .paper import Paper -BASE_FILE = 'papers.yaml' -BIB_DIR = 'bibdata' -META_DIR = 'meta' -DOC_DIR = 'doc' +def _base27(n): + return _base27((n - 1) // 26) + chr(ord('a') + ((n - 1) % 26)) if n else '' class CiteKeyCollision(Exception): @@ -23,199 +21,110 @@ class InvalidReference(Exception): class Repository(object): - def __init__(self, config, load=True): - """Initialize the repository. - - :param load: if load is True, load the repository from disk, - from path config.papers_dir. - """ + def __init__(self, config): self.config = config - self.citekeys = [] - if load: - self.load() - - # @classmethod - # def from_directory(cls, config, papersdir=None): - # repo = cls(config) - # if papersdir is None: - # papersdir = config.papers_dir - # repo.papersdir = files.clean_path(papersdir) - # repo.load() - # return repo + self._citekeys = None + self.databroker = datacache.DataCache(self.config.pubsdir) + + @property + def citekeys(self): + if self._citekeys is None: + self._citekeys = self.databroker.citekeys() + return self._citekeys def __contains__(self, citekey): - """Allows to use 'if citekey in repo' pattern""" + """ Allows to use 'if citekey in repo' pattern + + Warning: costly the first time. + """ return citekey in self.citekeys def __len__(self): + """Warning: costly the first time.""" return len(self.citekeys) - # load, save repo - def _init_dirs(self, autodoc=True): - """Create, if necessary, the repository directories. - - Should only be called by load or save. - """ - self.bib_dir = files.clean_path(self.config.papers_dir, BIB_DIR) - self.meta_dir = files.clean_path(self.config.papers_dir, META_DIR) - if self.config.doc_dir == 'doc': - self.doc_dir = files.clean_path(self.config.papers_dir, DOC_DIR) - else: - self.doc_dir = files.clean_path(self.config.doc_dir) - self.cfg_path = files.clean_path(self.config.papers_dir, 'papers.yaml') - - for d in [self.bib_dir, self.meta_dir, self.doc_dir]: - if not os.path.exists(d): - os.makedirs(d) - - def load(self): - """Load the repository, creating dirs if necessary""" - self._init_dirs() - repo_config = files.read_yamlfile(self.cfg_path) - self.citekeys = repo_config['citekeys'] - - def save(self): - """Save the repo, creating dirs if necessary""" - self._init_dirs() - repo_cfg = {'citekeys': self.citekeys} - files.write_yamlfile(self.cfg_path, repo_cfg) - - # reference - def ref2citekey(self, ref): - """Tries to get citekey from given reference. - Ref can be a citekey or a number. - """ - if ref in self.citekeys: - return ref - else: - try: - return self.citekeys[int(ref)] - except (IndexError, ValueError): - raise InvalidReference - # papers def all_papers(self): for key in self.citekeys: - yield self.get_paper(key) + yield self.pull_paper(key) - def get_paper(self, citekey): + def pull_paper(self, citekey): """Load a paper by its citekey from disk, if necessary.""" - if citekey in self.citekeys: - return PaperInRepo.load(self, self._bibfile(citekey), - self._metafile(citekey)) + if self.databroker.exists(paper.citekey, both = True): + return Paper(self, self.databroker.pull_bibdata(citekey), + self.databroker.pull_metadata(citekey)) else: raise InvalidReference - def _add_citekey(self, citekey): - if citekey not in self.citekeys: - self.citekeys.append(citekey) - self.save() - - def _write_paper(self, paper): - """Warning: overwrites the paper without checking if it exists.""" - paper.save(self._bibfile(paper.citekey), - self._metafile(paper.citekey)) - self._add_citekey(paper.citekey) - - def _remove_paper(self, citekey, remove_doc=True): - """ This version of remove is not meant to be accessed from outside. - It removes paper without raising the Remove Event""" - paper = self.get_paper(citekey) - self.citekeys.remove(citekey) - os.remove(self._metafile(citekey)) - os.remove(self._bibfile(citekey)) - # Eventually remove associated document + def push_paper(self, paper, overwrite=False, event=True): + """ Push a paper to disk + + :param overwrite: if False, mimick the behavior of adding a paper + if True, mimick the behavior of updating a paper + """ + bibstruct.check_citekey(paper.citekey) + if (not overwrite) and self.databroker.exists(paper.citekey, both = False): + raise IOError('files using this the {} citekey already exists'.format(citekey)) + if (not overwrite) and self.citekeys is not None and paper.citekey in self.citekeys: + raise CiteKeyCollision('citekey {} already in use'.format(paper.citekey)) + + self.databroker.push_bibdata(paper.citekey, paper.bibdata) + self.databroker.push_metadata(paper.citekey, paper.metadata) + self.citekeys.add(paper.citekey) + if event: + events.AddEvent(paper.citekey).send() + + def remove_paper(self, citekey, remove_doc=True, event=True): + """ Remove a paper. Is silent if nothing needs to be done.""" + + if event: + RemoveEvent(citekey).send() if remove_doc: try: - path = paper.get_document_path_in_repo() - os.remove(path) - except NoDocumentFile: - pass - self.save() - - def _move_doc(self, old_citekey, paper): - """Fragile. Make more robust""" - try: - old_docfile = self.find_document(old_citekey) - ext = os.path.splitext(old_docfile)[1] - new_docfile = os.path.join(self.doc_dir, paper.citekey + ext) - shutil.move(old_docfile, new_docfile) - paper.set_external_document(new_docfile) - except NoDocumentFile: - pass - - def _add_paper(self, paper, overwrite=False): - check_citekey(paper.citekey) - if not overwrite and paper.citekey in self.citekeys: - raise CiteKeyCollision('Citekey {} already in use'.format( - paper.citekey)) - self._write_paper(paper) - - # add, remove papers - def add_paper(self, paper): - self._add_paper(paper) - AddEvent(paper.citekey).send() - - def save_paper(self, paper, old_citekey=None, overwrite=False): - if old_citekey is None: - old_citekey = paper.citekey - if not old_citekey in self.citekeys: - raise ValueError('Paper not in repository, first add it.') - if old_citekey == paper.citekey: - self._write_paper(paper) - else: - self._add_paper(paper, overwrite=overwrite) # This checks for collisions - # We do not want to send the RemoveEvent, associated documents should be moved - self._remove_paper(old_citekey, remove_doc=False) - self._move_doc(old_citekey, paper) - RenameEvent(paper, old_citekey).send() - - def remove_paper(self, citekey, remove_doc=True): - RemoveEvent(citekey).send() - self._remove_paper(citekey, remove_doc) + metadata = self.databroker.pull_metadata(paper.citekey) + docpath = metadata.get('docfile', '') + self.databroker.remove_doc(docpath) + except IOError: + pass # FXME: if IOError is about being unable to + # remove the file, we need to issue an error.I - def _bibfile(self, citekey): - return os.path.join(self.bib_dir, citekey + '.bibyaml') - - def _metafile(self, citekey): - return os.path.join(self.meta_dir, citekey + '.meta') - - def generate_citekey(self, paper, citekey=None): - """Create a unique citekey for the given paper.""" - if citekey is None: - citekey = paper.generate_citekey() - for n in itertools.count(): - if not citekey + _base27(n) in self.citekeys: - return citekey + _base27(n) + self.citekeys.remove(citekey) + self.databroker.remove(citekey) - def find_document(self, citekey): - found = glob.glob('{}/{}.*'.format(self.doc_dir, citekey)) - if found: - return found[0] + def rename_paper(self, paper, new_citekey): + old_citekey = paper.citekey + # check if new_citekey is not the same as paper.citekey + if old_citekey == new_citekey: + push_paper(paper, overwrite=True, event=False) else: - raise NoDocumentFile + # check if new_citekey does not exists + if self.databroker.exists(new_citekey, both=False): + raise IOError("can't rename paper to {}, conflicting files exists".format(new_citekey)) + # modify bibdata + raise NotImplementedError + # move doc file if necessary + if self.databroker.is_pubsdir_doc(paper.docpath): + new_docpath = self.databroker.copy_doc(new_citekey, paper.docpath) + self.databroker.remove_doc(paper.docpath) + paper.docpath = new_docpath + + # push_paper to new_citekey + self.databroker.push(new_citekey, paper.metadata) + # remove_paper of old_citekey + self.databroker.remove(old_citekey) + # send event + RenameEvent(paper, old_citekey).send() - def import_document(self, citekey, doc_file): - if citekey not in self.citekeys: - raise ValueError("Unknown citekey: {}.".format(citekey)) - else: - if not os.path.isfile(doc_file): - raise ValueError("No file {} found.".format(doc_file)) - ext = os.path.splitext(doc_file)[1] - new_doc_file = os.path.join(self.doc_dir, citekey + ext) - shutil.copy(doc_file, new_doc_file) + def unique_citekey(self, base_key): + """Create a unique citekey for a given basekey.""" + for n in itertools.count(): + if not base_key + _base27(n) in self.citekeys: + return base_key + _base27(n) def get_tags(self): + """FIXME: bibdata doesn't need to be read.""" tags = set() for p in self.all_papers(): tags = tags.union(p.tags) return tags - -def _base27(n): - return _base27((n - 1) // 26) + chr(ord('a') + ((n - 1) % 26)) if n else '' - - -def _base(num, b): - q, r = divmod(num - 1, len(b)) - return _base(q, b) + b[r] if num else '' From dfd47688bbe43022c94d73039929ad44de63dad1 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:04:07 +0100 Subject: [PATCH 12/48] add command --- papers/commands/__init__.py | 2 +- papers/commands/add_cmd.py | 78 ++++++++++++++++++++++++++----------- papers/papers_cmd.py | 2 +- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index 03603a9..b1b4c16 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -1,5 +1,5 @@ import init_cmd -# import add_cmd +import add_cmd # import import_cmd # import export_cmd # import list_cmd diff --git a/papers/commands/add_cmd.py b/papers/commands/add_cmd.py index 2cdeed8..aaba145 100644 --- a/papers/commands/add_cmd.py +++ b/papers/commands/add_cmd.py @@ -1,10 +1,9 @@ -from .. import repo -from .. import files -from ..paper import Paper, NoDocumentFile, get_bibentry_from_string -from ..configs import config from ..uis import get_ui -from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata - +from ..configs import config +from .. import bibstruct +from .. import content +from .. import repo +from .. import paper def parser(subparsers): parser = subparsers.add_parser('add', help='add a paper to the repository') @@ -13,6 +12,8 @@ def parser(subparsers): parser.add_argument('-d', '--docfile', help='pdf or ps file', default=None) parser.add_argument('-t', '--tags', help='tags associated to the paper, separated by commas', default=None) + parser.add_argument('-k', '--citekey', help='citekey associated with the paper;\nif not provided, one will be generated automatically.', + default=None) parser.add_argument('-c', '--copy', action='store_true', default=None, help="copy document files into library directory (default)") parser.add_argument('-C', '--nocopy', action='store_false', dest='copy', @@ -30,41 +31,72 @@ def command(args): bibfile = args.bibfile docfile = args.docfile tags = args.tags - copy = args.copy + citekey = args.copy - if copy is None: - copy = config().import_copy rp = repo.Repository(config()) + + # get bibfile + if bibfile is None: cont = True bibstr = '' while cont: try: - bibstr = files.editor_input(config().edit_cmd, bibstr, suffix='.yaml') - key, bib = get_bibentry_from_string(bibstr) + bibstr = content.editor_input(config().edit_cmd, bibstr, suffix='.yaml') + bibdata = rp.databroker.verify(bibstr) + bibstruct.verify_bibdata(bibdata) + # REFACTOR Generate citykey cont = False - except Exception: + except ValueError: cont = ui.input_yn( question='Invalid bibfile. Edit again ?', default='y') if not cont: ui.exit(0) - p = Paper(bibentry=bib, citekey=key) else: - p = Paper.load(bibfile) + bibdata_raw = content.get_content(bibfile) + bibdata = rp.databroker.verify(bibdata_raw) + if bibdata is None: + ui.error('invalid bibfile {}.'.format(bibfile)) + + print bibdata + # citekey + + citekey = args.citekey + if citekey is None: + base_key = bibstruct.extract_citekey(bibdata) + citekey = rp.unique_citekey(base_key) + else: + rp.databroker.exists(citekey, both=False) + + # tags + if tags is not None: p.tags = set(tags.split(',')) - # Check if another doc file is specified in bibtex - docfile2 = extract_doc_path_from_bibdata(p) + + p = paper.Paper(citekey=citekey, bibdata=bibdata) + + # document file + + bib_docfile = bibstruct.extract_docfile(bibdata) if docfile is None: - docfile = docfile2 - elif docfile2 is not None: - ui.warning( - "Skipping document file from bib file: %s, using %s instead." - % (docfile2, docfile)) + docfile = bib_docfile + elif bib_docfile is not None: + ui.warning(('Skipping document file from bib file ' + '{}, using {} instead.').format(bib_docfile, docfile)) + + if docfile is not None: + copy_doc = args.copy + if copy_doc is None: + copy_doc = config().import_copy + if copy_doc: + docfile = rp.databroker.copy_doc(citekey, docfile) + + # create the paper + try: - add_paper_with_docfile(rp, p, docfile=docfile, copy=copy) + p.docpath = docfile + rp.push_paper(p) except ValueError, v: ui.error(v.message) ui.exit(1) -# TODO handle case where citekey exists diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index dfd93e9..35b50ec 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -14,7 +14,7 @@ from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), - # ('add', commands.add_cmd), + ('add', commands.add_cmd), # ('import', commands.import_cmd), # ('export', commands.export_cmd), # ('list', commands.list_cmd), From 516996f8fb8bdd000f9ca26acd63584a3e2e8bd6 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:08:27 +0100 Subject: [PATCH 13/48] removed files.py --- papers/files.py | 221 ------------------------------------------------ 1 file changed, 221 deletions(-) delete mode 100644 papers/files.py diff --git a/papers/files.py b/papers/files.py deleted file mode 100644 index 1937870..0000000 --- a/papers/files.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -This module can't depend on configs. -If you feel the need to import configs, you are not in the right place. -""" -from __future__ import print_function - -import os -import subprocess -import tempfile -from .p3 import io -from io import StringIO - -import yaml - -from . import color - -try: - import pybtex - import pybtex.database - import pybtex.database.input - import pybtex.database.input.bibtex - import pybtex.database.input.bibtexml - import pybtex.database.input.bibyaml - import pybtex.database.output - import pybtex.database.output.bibtex - import pybtex.database.output.bibtexml - import pybtex.database.output.bibyaml - -except ImportError: - print(color.dye('error', color.error) + - ": you need to install Pybtex; try running 'pip install " - "pybtex' or 'easy_install pybtex'") - exit(-1) - -_papersdir = None - -BIB_EXTENSIONS = ['.bib', '.bibyaml', '.bibml', '.yaml'] -FORMATS_INPUT = {'bib' : pybtex.database.input.bibtex, - 'xml' : pybtex.database.input.bibtexml, - 'yml' : pybtex.database.input.bibyaml, - 'yaml' : pybtex.database.input.bibyaml, - 'bibyaml': pybtex.database.input.bibyaml} -FORMATS_OUTPUT = {'bib' : pybtex.database.output.bibtex, - 'bibtex' : pybtex.database.output.bibtex, - 'xml' : pybtex.database.output.bibtexml, - 'yml' : pybtex.database.output.bibyaml, - 'yaml' : pybtex.database.output.bibyaml, - 'bibyaml': pybtex.database.output.bibyaml} - - -def clean_path(*args): - return os.path.abspath(os.path.expanduser(os.path.join(*args))) - - -def name_from_path(fullpdfpath, verbose=False): - name, ext = os.path.splitext(os.path.split(fullpdfpath)[1]) - if verbose: - if ext != '.pdf' and ext != '.ps': - print('{}: extension {} not recognized'.format( - color.dye('warning', color.warning), - color.dye(ext, color.cyan))) - return name, ext - - -def check_directory(path, fail=False): - if fail: - if not os.path.exists(path): - raise IOError("File does not exist: {}.".format(path)) - if not os.path.isdir(path): - raise IOError("{} is not a directory.".format(path)) - return True - else: - return os.path.exists(path) and os.path.isdir(path) - - -def check_file(path, fail=False): - if fail: - if not os.path.exists(path): - raise IOError("File does not exist: {}.".format(path)) - if not os.path.isfile(path): - raise IOError("{} is not a file.".format(path)) - return True - else: - return os.path.exists(path) and os.path.isfile(path) - - -# yaml I/O - -def write_yamlfile(filepath, datamap): - try: - with open(filepath, 'w') as f: - yaml.dump(datamap, f) - except IOError: - print('{}: impossible to read or write on file {}'.format( - color.dye('error', color.error), - color.dye(filepath, color.filepath))) - exit(-1) - - -def read_yamlfile(filepath): - check_file(filepath, fail=True) - try: - with open(filepath, 'r') as f: - return yaml.load(f) - except IOError: - print('{}: impossible to read file {}'.format( - color.dye('error', color.error), - color.dye(filepath, color.filepath))) - exit(-1) - - -def load_bibdata(filename, filepath): - return load_externalbibfile(filepath) - - -def write_bibdata(bib_data, file_, format_): - writer = FORMATS_OUTPUT[format_].Writer() - writer.write_stream(bib_data, file_) - - -def save_bibdata(bib_data, filepath): - with open(filepath, 'w') as f: - write_bibdata(bib_data, f, 'yaml') - - -def save_meta(meta_data, filepath): - new_meta = meta_data.copy() - # Cannot store sets in yaml - new_meta['tags'] = list(new_meta['tags']) - write_yamlfile(filepath, new_meta) - - -# is this function ever used? 08/06/2013 -def load_meta(filepath): - return read_yamlfile(filepath) - - -# specific to bibliography data - -def load_externalbibfile(fullbibpath): - check_file(fullbibpath, fail=True) - filename, ext = os.path.splitext(os.path.split(fullbibpath)[1]) - if ext[1:] in list(FORMATS_INPUT.keys()): - with open(fullbibpath) as f: - return _parse_bibdata_formated_stream(f, ext[1:]) - else: - print('{}: {} not recognized format for bibliography'.format( - color.dye('error', color.error), - color.dye(ext, color.cyan))) - exit(-1) - - -def _parse_bibdata_formated_stream(stream, fmt): - """Parse a stream for bibdata, using the supplied format.""" - try: - parser = FORMATS_INPUT[fmt].Parser() - data = parser.parse_stream(stream) - if len(list(data.entries.keys())) > 0: - return data - except Exception: - pass - raise ValueError('content format is not recognized.') - -def parse_bibdata(content, format_=None): - """Parse bib data from string or stream. - - Raise ValueError if no bibdata is present. - :content: stream - :param format_: (bib|xml|yml) if format is None, tries to recognize the - format automatically. - """ - fmts = [format_] - if format_ is None: - fmts = FORMATS_INPUT.keys() - # we need to reuse the content - content = content if type(content) == str else str(content.read()) - - # If you use StingIO from io then the content must be unicode - # Let call this quick fix a hack but we should think it more carefully - content = unicode(content) - # This bug was really a pain in the ass to discover because of the (old) except Expection below! - # I changed it to the only kind of error that can raise _parse_bibdata_formated_stream, which is a ValueError - - for fmt in fmts: - try: - return _parse_bibdata_formated_stream(StringIO(content), fmt) - except ValueError: - pass - - raise ValueError('content format is not recognized.') - - -def editor_input(editor, initial="", suffix=None): - """Use an editor to get input""" - if suffix is None: - suffix = '.tmp' - with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: - tfile_name = temp_file.name - temp_file.write(initial) - temp_file.flush() - cmd = editor.split() # this enable editor command with option, e.g. gvim -f - cmd.append(tfile_name) - subprocess.call(cmd) - with open(tfile_name) as temp_file: - content = temp_file.read() - os.remove(tfile_name) - return content - - -def edit_file(editor, path_to_file, temporary=True): - if temporary: - check_file(path_to_file, fail=True) - with open(path_to_file) as f: - content = f.read() - content = editor_input(editor, content) - with open(path_to_file, 'w') as f: - f.write(content) - else: - cmd = editor.split() # this enable editor command with option, e.g. gvim -f - cmd.append(path_to_file) - subprocess.call(cmd) From 76a6d092dde19df5ae673d16b0b866623f71488f Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:43:46 +0100 Subject: [PATCH 14/48] fixed bug in datacache + added corresponding tests --- papers/datacache.py | 2 +- tests/test_databroker.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/papers/datacache.py b/papers/datacache.py index 0d75fb3..bb0b97c 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -47,7 +47,7 @@ class DataCache(object): self.databroker.remove(citekey) def exists(self, citekey, both=True): - self.databroker.exists(citekey, both=both) + return self.databroker.exists(citekey, both=both) def citekeys(self): listings = self.listing(filestats=False) diff --git a/tests/test_databroker.py b/tests/test_databroker.py index 8cb6344..255585b 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -28,12 +28,17 @@ class TestDataBroker(TestFakeFs): page99_metadata = ende.decode_metadata(str_fixtures.metadata_raw0) page99_bibdata = ende.decode_bibdata(str_fixtures.bibyaml_raw0) - dtb = databroker.DataBroker('tmp', create=True) - dtc = datacache.DataCache('tmp') + for db_class in [databroker.DataBroker, datacache.DataCache]: + self.fs = fake_env.create_fake_fs([content, filebroker]) + + db = db_class('tmp', create=True) - for db in [dtb, dtc]: db.push_metadata('citekey1', page99_metadata) + self.assertTrue(db.exists('citekey1', both=False)) + self.assertFalse(db.exists('citekey1', both=True)) + db.push_bibdata('citekey1', page99_bibdata) + self.assertTrue(db.exists('citekey1', both=True)) self.assertEqual(db.pull_metadata('citekey1'), page99_metadata) self.assertEqual(db.pull_bibdata('citekey1'), page99_bibdata) From 353a282b42e1f16aecdd4f10c2a407c3664db879 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:57:37 +0100 Subject: [PATCH 15/48] minor bug in repo; self.bibentry in paper --- papers/paper.py | 4 +++- papers/repo.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/papers/paper.py b/papers/paper.py index 19a3dbd..feb8aa9 100644 --- a/papers/paper.py +++ b/papers/paper.py @@ -18,7 +18,9 @@ class Paper(object): self.citekey = citekey self.metadata = metadata self.bibdata = bibdata - + + _, self.bibentry = bibstruct.get_entry(self.bibdata) + if self.metadata is None: self.metadata = copy.deepcopy(DEFAULT_META) if self.citekey is None: diff --git a/papers/repo.py b/papers/repo.py index 113be51..ef7fe18 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -50,9 +50,10 @@ class Repository(object): def pull_paper(self, citekey): """Load a paper by its citekey from disk, if necessary.""" - if self.databroker.exists(paper.citekey, both = True): - return Paper(self, self.databroker.pull_bibdata(citekey), - self.databroker.pull_metadata(citekey)) + if self.databroker.exists(citekey, both = True): + return Paper(self.databroker.pull_bibdata(citekey), + citekey=citekey, + metadata=self.databroker.pull_metadata(citekey)) else: raise InvalidReference From 017a31460d434c539b49fa75684a17457fff4d91 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 02:59:28 +0100 Subject: [PATCH 16/48] updated list cmd --- papers/commands/__init__.py | 2 +- papers/commands/add_cmd.py | 3 +-- papers/commands/list_cmd.py | 4 ++-- papers/papers_cmd.py | 2 +- papers/pretty.py | 13 +++++++++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index b1b4c16..f20dd9d 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -1,8 +1,8 @@ import init_cmd import add_cmd +import list_cmd # import import_cmd # import export_cmd -# import list_cmd # import edit_cmd # import remove_cmd # import open_cmd diff --git a/papers/commands/add_cmd.py b/papers/commands/add_cmd.py index aaba145..f35301d 100644 --- a/papers/commands/add_cmd.py +++ b/papers/commands/add_cmd.py @@ -59,7 +59,6 @@ def command(args): if bibdata is None: ui.error('invalid bibfile {}.'.format(bibfile)) - print bibdata # citekey citekey = args.citekey @@ -74,7 +73,7 @@ def command(args): if tags is not None: p.tags = set(tags.split(',')) - p = paper.Paper(citekey=citekey, bibdata=bibdata) + p = paper.Paper(bibdata, citekey=citekey) # document file diff --git a/papers/commands/list_cmd.py b/papers/commands/list_cmd.py index 498785d..64956ca 100644 --- a/papers/commands/list_cmd.py +++ b/papers/commands/list_cmd.py @@ -1,5 +1,5 @@ from .. import repo -from . import helpers +from .. import pretty from ..configs import config from ..uis import get_ui @@ -29,7 +29,7 @@ def command(args): filter_paper(p, args.query, case_sensitive=args.case_sensitive), enumerate(rp.all_papers())) ui.print_('\n'.join( - helpers.paper_oneliner(p, n=n, citekey_only=args.citekeys) + pretty.paper_oneliner(p, n=n, citekey_only=args.citekeys) for n, p in papers)) diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 35b50ec..ea0c67b 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -15,9 +15,9 @@ from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), ('add', commands.add_cmd), + ('list', commands.list_cmd), # ('import', commands.import_cmd), # ('export', commands.export_cmd), - # ('list', commands.list_cmd), # ('edit', commands.edit_cmd), # ('remove', commands.remove_cmd), # ('open', commands.open_cmd), diff --git a/papers/pretty.py b/papers/pretty.py index 7fa7e83..8b79da2 100644 --- a/papers/pretty.py +++ b/papers/pretty.py @@ -50,3 +50,16 @@ def bib_desc(bib_data): s += '\n' s += '\n'.join('{}: {}'.format(k, v) for k, v in article.fields.items()) return s + + +def paper_oneliner(p, n = 0, citekey_only = False): + if citekey_only: + return p.citekey + else: + bibdesc = bib_oneliner(p.bibentry) + return (u'[{citekey}] {descr} {tags}'.format( + citekey=color.dye(p.citekey, color.purple), + descr=bibdesc, + tags=color.dye(' '.join(p.tags), + color.purple, bold=True), + )).encode('utf-8') \ No newline at end of file From da3e70649b725593c98d35a40490de40563ef895 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 03:13:19 +0100 Subject: [PATCH 17/48] updated open and websearch commands --- papers/commands/__init__.py | 4 ++-- papers/commands/open_cmd.py | 27 ++++++++++++++------------- papers/papers_cmd.py | 4 ++-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index f20dd9d..179e7ca 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -1,12 +1,12 @@ import init_cmd import add_cmd import list_cmd +import open_cmd +import websearch_cmd # import import_cmd # import export_cmd # import edit_cmd # import remove_cmd -# import open_cmd -# import websearch_cmd # import tag_cmd # import attach_cmd # import update_cmd diff --git a/papers/commands/open_cmd.py b/papers/commands/open_cmd.py index 670899e..3807cb2 100644 --- a/papers/commands/open_cmd.py +++ b/papers/commands/open_cmd.py @@ -1,11 +1,10 @@ import subprocess from .. import repo -from ..paper import NoDocumentFile from ..configs import config from ..uis import get_ui from .. import color -from .helpers import add_references_argument, parse_reference +#from .helpers import add_references_argument, parse_reference def parser(subparsers): @@ -13,7 +12,8 @@ def parser(subparsers): help='open the paper in a pdf viewer') parser.add_argument('-w', '--with', dest='with_command', default=None, help='command to use to open the document file') - add_references_argument(parser, single=True) + parser.add_argument('citekey', + help='citekey of the paper') return parser @@ -21,23 +21,24 @@ def command(args): ui = get_ui() with_command = args.with_command - reference = args.reference + citekey = args.citekey rp = repo.Repository(config()) - key = parse_reference(rp, reference) - paper = rp.get_paper(key) + paper = rp.pull_paper(citekey) if with_command is None: with_command = config().open_cmd + + if paper.docpath is None: + ui.error('No document associated with the entry {}.'.format( + color.dye(citekey, color.citekey))) + ui.exit() + try: - filepath = paper.get_document_path() + docpath = rp.databroker.real_docpath(paper.docpath) cmd = with_command.split() - cmd.append(filepath) + cmd.append(docpath) subprocess.Popen(cmd) - ui.print_('{} opened.'.format(color.dye(filepath, color.filepath))) - except NoDocumentFile: - ui.error('No document associated with the entry {}.'.format( - color.dye(key, color.citekey))) - ui.exit() + ui.print_('{} opened.'.format(color.dye(docpath, color.filepath))) except OSError: ui.error("Command does not exist: %s." % with_command) ui.exit(127) diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index ea0c67b..58e0f6c 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -16,12 +16,12 @@ CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), ('add', commands.add_cmd), ('list', commands.list_cmd), + ('open', commands.open_cmd), + ('websearch', commands.websearch_cmd), # ('import', commands.import_cmd), # ('export', commands.export_cmd), # ('edit', commands.edit_cmd), # ('remove', commands.remove_cmd), - # ('open', commands.open_cmd), - # ('websearch', commands.websearch_cmd), # ('tag', commands.tag_cmd), # ('attach', commands.attach_cmd), # ('update', commands.update_cmd), From c6d7300ae3b5970655004498bdf8282fa35cf215 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 03:32:19 +0100 Subject: [PATCH 18/48] updated remove cmd --- papers/commands/__init__.py | 2 +- papers/commands/remove_cmd.py | 18 ++++++++---------- papers/databroker.py | 4 ++-- papers/datacache.py | 4 ++-- papers/filebroker.py | 15 ++++++++++----- papers/papers_cmd.py | 2 +- papers/repo.py | 8 ++++---- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index 179e7ca..3e533a7 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -3,10 +3,10 @@ import add_cmd import list_cmd import open_cmd import websearch_cmd +import remove_cmd # import import_cmd # import export_cmd # import edit_cmd -# import remove_cmd # import tag_cmd # import attach_cmd # import update_cmd diff --git a/papers/commands/remove_cmd.py b/papers/commands/remove_cmd.py index 76e7e6e..5351c8f 100644 --- a/papers/commands/remove_cmd.py +++ b/papers/commands/remove_cmd.py @@ -2,14 +2,14 @@ from .. import repo from .. import color from ..configs import config from ..uis import get_ui -from .helpers import add_references_argument, parse_references def parser(subparsers): parser = subparsers.add_parser('remove', help='removes a paper') parser.add_argument('-f', '--force', action='store_true', default=None, - help="does not prompt for confirmation.") - add_references_argument(parser) + help="does not prompt for confirmation.") + parser.add_argument('citekeys', nargs='*', + help="one or several citekeys") return parser @@ -17,15 +17,13 @@ def command(args): ui = get_ui() force = args.force - references = args.references - rp = repo.Repository(config()) - citekeys = parse_references(rp, references) + if force is None: - are_you_sure = ("Are you sure you want to delete paper(s) [%s]" - " (this will also delete associated documents)?" - % ', '.join([color.dye(c, color.citekey) for c in citekeys])) + are_you_sure = (("Are you sure you want to delete paper(s) [{}]" + " (this will also delete associated documents)?") + .format(', '.join([color.dye(c, color.citekey) for c in args.citekeys]))) sure = ui.input_yn(question=are_you_sure, default='n') if force or sure: - for c in citekeys: + for c in args.citekeys: rp.remove_paper(c) diff --git a/papers/databroker.py b/papers/databroker.py index fed7307..a69f988 100644 --- a/papers/databroker.py +++ b/papers/databroker.py @@ -58,8 +58,8 @@ class DataBroker(object): def copy_doc(self, citekey, source_path, overwrite=False): return self.docbroker.copy_doc(citekey, source_path, overwrite=overwrite) - def remove_doc(self, docpath): - return self.docbroker.remove_doc(docpath) + def remove_doc(self, docpath, silent=True): + return self.docbroker.remove_doc(docpath, silent=silent) def real_docpath(self, docpath): return self.docbroker.real_docpath(docpath) \ No newline at end of file diff --git a/papers/datacache.py b/papers/datacache.py index bb0b97c..9859699 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -68,8 +68,8 @@ class DataCache(object): def copy_doc(self, citekey, source_path, overwrite=False): return self.databroker.copy_doc(citekey, source_path, overwrite=overwrite) - def remove_doc(self, docpath): - return self.databroker.remove_doc(docpath) + def remove_doc(self, docpath, silent=True): + return self.databroker.remove_doc(docpath, silent=silent) def real_docpath(self, docpath): return self.databroker.real_docpath(docpath) diff --git a/papers/filebroker.py b/papers/filebroker.py index 18a7452..aa43e74 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -118,7 +118,10 @@ class DocBroker(object): os.mkdir(self.docdir) def is_pubsdir_doc(self, docpath): - parsed = urlparse.urlparse(docpath) + try: + parsed = urlparse.urlparse(docpath) + except Exception: + return False if parsed.scheme == 'pubsdir': assert parsed.netloc == 'doc' assert parsed.path[0] == '/' @@ -143,14 +146,16 @@ class DocBroker(object): return target_path - def remove_doc(self, docpath): + def remove_doc(self, docpath, silent=True): """ Will remove only file hosted in pubsdir://doc/ - :raise ValueError: for other paths. + :raise ValueError: for other paths, unless :param silent: is True """ if not self.is_pubsdir_doc(docpath): - raise ValueError(('the file to be removed {} is set as external. ' - 'you should remove it manually.').format(docpath)) + if not silent: + raise ValueError(('the file to be removed {} is set as external. ' + 'you should remove it manually.').format(docpath)) + return filepath = self.real_docpath(docpath) if check_file(filepath): os.remove(filepath) diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 58e0f6c..7ab156b 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -18,10 +18,10 @@ CORE_CMDS = collections.OrderedDict([ ('list', commands.list_cmd), ('open', commands.open_cmd), ('websearch', commands.websearch_cmd), + ('remove', commands.remove_cmd), # ('import', commands.import_cmd), # ('export', commands.export_cmd), # ('edit', commands.edit_cmd), - # ('remove', commands.remove_cmd), # ('tag', commands.tag_cmd), # ('attach', commands.attach_cmd), # ('update', commands.update_cmd), diff --git a/papers/repo.py b/papers/repo.py index ef7fe18..cb60e6f 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -79,12 +79,12 @@ class Repository(object): """ Remove a paper. Is silent if nothing needs to be done.""" if event: - RemoveEvent(citekey).send() + events.RemoveEvent(citekey).send() if remove_doc: try: - metadata = self.databroker.pull_metadata(paper.citekey) - docpath = metadata.get('docfile', '') - self.databroker.remove_doc(docpath) + metadata = self.databroker.pull_metadata(citekey) + docpath = metadata.get('docfile') + self.databroker.remove_doc(docpath, silent=True) except IOError: pass # FXME: if IOError is about being unable to # remove the file, we need to issue an error.I From 24df1b36ae30467ec809e4861247e8aa01da7967 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 12:45:05 +0100 Subject: [PATCH 19/48] tag cmd --- papers/commands/__init__.py | 2 +- papers/commands/tag_cmd.py | 28 +++++++++++++--------------- papers/commands/update_cmd.py | 1 - papers/papers_cmd.py | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index 3e533a7..e415aa0 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -4,9 +4,9 @@ import list_cmd import open_cmd import websearch_cmd import remove_cmd +import tag_cmd # import import_cmd # import export_cmd # import edit_cmd -# import tag_cmd # import attach_cmd # import update_cmd diff --git a/papers/commands/tag_cmd.py b/papers/commands/tag_cmd.py index cfebd49..2395a9d 100644 --- a/papers/commands/tag_cmd.py +++ b/papers/commands/tag_cmd.py @@ -18,17 +18,16 @@ The different use cases are : """ from ..repo import Repository, InvalidReference -from . import helpers from ..configs import config from ..uis import get_ui +from .. import pretty def parser(subparsers): parser = subparsers.add_parser('tag', help="add, remove and show tags") - parser.add_argument('referenceOrTag', nargs='?', default = None, - help='reference to the paper (citekey or number), or ' - 'tag.') + parser.add_argument('citekeyOrTag', nargs='?', default = None, + help='citekey or tag.') parser.add_argument('tags', nargs='?', default = None, - help='If the previous argument was a reference, then ' + help='If the previous argument was a citekey, then ' 'then a list of tags separated by a +.') # TODO find a way to display clear help for multiple command semantics, # indistinguisable for argparse. (fabien, 201306) @@ -69,19 +68,18 @@ def command(args): """Add, remove and show tags""" ui = get_ui() - referenceOrTag = args.referenceOrTag + citekeyOrTag = args.citekeyOrTag tags = args.tags rp = Repository(config()) - if referenceOrTag is None: + if citekeyOrTag is None: for tag in rp.get_tags(): ui.print_(tag) else: - try: - citekey = rp.ref2citekey(referenceOrTag) - p = rp.get_paper(citekey) + if rp.databroker.exists(citekeyOrTag): + p = rp.pull_paper(citekeyOrTag) if tags is None: ui.print_(' '.join(p.tags)) else: @@ -90,15 +88,15 @@ def command(args): p.add_tag(tag) for tag in remove_tags: p.remove_tag(tag) - rp.save_paper(p) - except InvalidReference: - # case where we want to find paper with specific tags - included, excluded = _tag_groups(_parse_tags(referenceOrTag)) + rp.push_paper(p, overwrite=True) + else: + # case where we want to find papers with specific tags + included, excluded = _tag_groups(_parse_tags(citekeyOrTag)) papers_list = [] for n, p in enumerate(rp.all_papers()): if (p.tags.issuperset(included) and len(p.tags.intersection(excluded)) == 0): papers_list.append((p, n)) - ui.print_('\n'.join(helpers.paper_oneliner(p, n) + ui.print_('\n'.join(pretty.paper_oneliner(p, n) for p, n in papers_list)) diff --git a/papers/commands/update_cmd.py b/papers/commands/update_cmd.py index 312cb63..e693090 100644 --- a/papers/commands/update_cmd.py +++ b/papers/commands/update_cmd.py @@ -44,7 +44,6 @@ def command(args): if repo_version == 2: # update config - print 'bla' cfg_update = [('papers-directory', 'papers_dir'), ('open-cmd', 'open_cmd'), ('edit-cmd', 'edit_cmd'), diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 7ab156b..7e5f94d 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -19,10 +19,10 @@ CORE_CMDS = collections.OrderedDict([ ('open', commands.open_cmd), ('websearch', commands.websearch_cmd), ('remove', commands.remove_cmd), + ('tag', commands.tag_cmd), # ('import', commands.import_cmd), # ('export', commands.export_cmd), # ('edit', commands.edit_cmd), - # ('tag', commands.tag_cmd), # ('attach', commands.attach_cmd), # ('update', commands.update_cmd), ]) From bc82d0de8c99bb43abdf1303e5cadb9eeb6a94b5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 14:48:12 +0100 Subject: [PATCH 20/48] update attach and export cmds --- papers/commands/__init__.py | 4 ++-- papers/commands/attach_cmd.py | 30 +++++++++++++++++------------- papers/commands/export_cmd.py | 23 +++++++++++++++-------- papers/endecoder.py | 18 ++++++++++++------ papers/papers_cmd.py | 4 ++-- papers/repo.py | 2 +- 6 files changed, 49 insertions(+), 32 deletions(-) diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index e415aa0..d5b7100 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -5,8 +5,8 @@ import open_cmd import websearch_cmd import remove_cmd import tag_cmd +import attach_cmd +import export_cmd # import import_cmd -# import export_cmd # import edit_cmd -# import attach_cmd # import update_cmd diff --git a/papers/commands/attach_cmd.py b/papers/commands/attach_cmd.py index 3a4b00a..a3caf52 100644 --- a/papers/commands/attach_cmd.py +++ b/papers/commands/attach_cmd.py @@ -1,9 +1,6 @@ from .. import repo from ..configs import config from ..uis import get_ui -from .helpers import (add_references_argument, parse_reference, - add_docfile_to_paper) - def parser(subparsers): parser = subparsers.add_parser('attach', @@ -12,8 +9,10 @@ def parser(subparsers): help="copy document files into library directory (default)") parser.add_argument('-C', '--nocopy', action='store_false', dest='copy', help="don't copy document files (opposite of -c)") - add_references_argument(parser, single=True) - parser.add_argument('document', help='pdf or ps file') + parser.add_argument('citekey', + help='citekey of the paper') + parser.add_argument('document', + help='document file') return parser @@ -24,19 +23,24 @@ def command(args): """ ui = get_ui() - copy = args.copy - reference = args.reference - document = args.document + rp = repo.Repository(config()) + paper = rp.pull_paper(args.citekey) + copy = args.copy if copy is None: copy = config().import_copy - rp = repo.Repository(config()) - key = parse_reference(rp, reference) - paper = rp.get_paper(key) + try: - add_docfile_to_paper(rp, paper, docfile=document, copy=copy) + document = args.document + if copy: + document = rp.databroker.copy_doc(paper.citekey, document) + else: + pass # TODO warn if file does not exists + paper.docpath = document except ValueError, v: ui.error(v.message) ui.exit(1) -# TODO handle case where citekey exists + except IOError, v: + ui.error(v.message) + ui.exit(1) diff --git a/papers/commands/export_cmd.py b/papers/commands/export_cmd.py index 37e5d46..1a729ea 100644 --- a/papers/commands/export_cmd.py +++ b/papers/commands/export_cmd.py @@ -1,19 +1,20 @@ +from __future__ import print_function import sys from pybtex.database import BibliographyData from .. import repo -from .. import files -from .helpers import parse_references, add_references_argument from ..configs import config from ..uis import get_ui +from .. import endecoder def parser(subparsers): parser = subparsers.add_parser('export', help='export bibliography') parser.add_argument('-f', '--bib-format', default='bibtex', - help="export format") - add_references_argument(parser) + help='export format') + parser.add_argument('citekeys', nargs='*', + help='one or several citekeys') return parser @@ -24,17 +25,23 @@ def command(args): ui = get_ui() bib_format = args.bib_format - references = args.references rp = repo.Repository(config()) - papers = [rp.get_paper(c) - for c in parse_references(rp, references)] + + try: + papers = [rp.pull_paper(c) for c in args.citekeys] + except repo.InvalidReference, v: + ui.error(v) + ui.exit(1) + if len(papers) == 0: papers = rp.all_papers() bib = BibliographyData() for p in papers: bib.add_entry(p.citekey, p.bibentry) try: - files.write_bibdata(bib, sys.stdout, bib_format) + exporter = endecoder.EnDecoder() + bibdata_raw = exporter.encode_bibdata(bib, fmt=bib_format) + print(bibdata_raw, end='') except KeyError: ui.error("Invalid output format: %s." % bib_format) diff --git a/papers/endecoder.py b/papers/endecoder.py index d264603..2febb27 100644 --- a/papers/endecoder.py +++ b/papers/endecoder.py @@ -10,6 +10,8 @@ try: import pybtex.database.input.bibtex import pybtex.database.input.bibtexml import pybtex.database.input.bibyaml + import pybtex.database.output.bibtex + import pybtex.database.output.bibtexml import pybtex.database.output.bibyaml except ImportError: @@ -30,9 +32,13 @@ class EnDecoder(object): * encode_bibdata will try to recognize exceptions """ - decode_fmt = (pybtex.database.input.bibyaml, - pybtex.database.input.bibtex, - pybtex.database.input.bibtexml) + decode_fmt = {'bibyaml' : pybtex.database.input.bibyaml, + 'bibtex' : pybtex.database.input.bibtex, + 'bibtexml': pybtex.database.input.bibtexml} + + encode_fmt = {'bibyaml' : pybtex.database.output.bibyaml, + 'bibtex' : pybtex.database.output.bibtex, + 'bibtexml': pybtex.database.output.bibtexml} def encode_metadata(self, metadata): return yaml.safe_dump(metadata, allow_unicode=True, encoding='UTF-8', indent = 4) @@ -40,16 +46,16 @@ class EnDecoder(object): def decode_metadata(self, metadata_raw): return yaml.safe_load(metadata_raw) - def encode_bibdata(self, bibdata): + def encode_bibdata(self, bibdata, fmt='bibyaml'): """Encode bibdata """ s = StringIO.StringIO() - pybtex.database.output.bibyaml.Writer().write_stream(bibdata, s) + EnDecoder.encode_fmt[fmt].Writer().write_stream(bibdata, s) return s.getvalue() def decode_bibdata(self, bibdata_raw): """""" bibdata_rawutf8 = unicode(bibdata_raw) - for fmt in EnDecoder.decode_fmt: + for fmt in EnDecoder.decode_fmt.values(): try: bibdata_stream = StringIO.StringIO(bibdata_rawutf8) return self._decode_bibdata(bibdata_stream, fmt.Parser()) diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 7e5f94d..9135371 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -20,10 +20,10 @@ CORE_CMDS = collections.OrderedDict([ ('websearch', commands.websearch_cmd), ('remove', commands.remove_cmd), ('tag', commands.tag_cmd), + ('attach', commands.attach_cmd), + ('export', commands.export_cmd), # ('import', commands.import_cmd), - # ('export', commands.export_cmd), # ('edit', commands.edit_cmd), - # ('attach', commands.attach_cmd), # ('update', commands.update_cmd), ]) diff --git a/papers/repo.py b/papers/repo.py index cb60e6f..557968d 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -55,7 +55,7 @@ class Repository(object): citekey=citekey, metadata=self.databroker.pull_metadata(citekey)) else: - raise InvalidReference + raise InvalidReference('{} citekey not found'.format(citekey)) def push_paper(self, paper, overwrite=False, event=True): """ Push a paper to disk From 8c54b19207c62e2850c1c0f5c03fe91293ac0344 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 19:35:21 +0100 Subject: [PATCH 21/48] updated import and added rename command --- papers/commands/__init__.py | 14 ++- papers/commands/import_cmd.py | 73 +++++++++++-- papers/commands/rename_cmd.py | 27 +++++ papers/databroker.py | 2 +- papers/datacache.py | 2 +- papers/endecoder.py | 2 + papers/papers_cmd.py | 12 ++- papers/repo.py | 18 ++-- tests/fake_env.py | 4 +- tests/str_fixtures.py | 16 +++ tests/test_usecase.py | 192 +++++++--------------------------- 11 files changed, 179 insertions(+), 183 deletions(-) create mode 100644 papers/commands/rename_cmd.py diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index d5b7100..f832ba3 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -1,12 +1,18 @@ +# core import init_cmd import add_cmd +import rename_cmd +import remove_cmd import list_cmd +# doc +import attach_cmd import open_cmd -import websearch_cmd -import remove_cmd import tag_cmd -import attach_cmd +# bulk import export_cmd -# import import_cmd +import import_cmd +# bonus +import websearch_cmd + # import edit_cmd # import update_cmd diff --git a/papers/commands/import_cmd.py b/papers/commands/import_cmd.py index c917949..0f94ad4 100644 --- a/papers/commands/import_cmd.py +++ b/papers/commands/import_cmd.py @@ -1,10 +1,17 @@ +import os + +from pybtex.database import Entry, BibliographyData, FieldDict, Person + from .. import repo +from .. import endecoder +from .. import bibstruct +from .. import color from ..paper import Paper -from .helpers import add_paper_with_docfile, extract_doc_path_from_bibdata from ..configs import config from ..uis import get_ui + def parser(subparsers): parser = subparsers.add_parser('import', help='import paper(s) to the repository') @@ -19,9 +26,46 @@ def parser(subparsers): return parser +def many_from_path(bibpath): + """Extract list of papers found in bibliographic files in path. + + The behavior is to: + - ignore wrong entries, + - overwrite duplicated entries. + :returns: dictionary of (key, paper | exception) + if loading of entry failed, the excpetion is returned in the + dictionary in place of the paper + """ + coder = endecoder.EnDecoder() + + bibpath = os.path.expanduser(bibpath) + if os.path.isdir(bibpath): + all_files = [os.path.join(bibpath, f) for f in os.listdir(bibpath) + if os.path.splitext(f)[-1][1:] in list(coder.decode_fmt.keys())] + else: + all_files = [bibpath] + + biblist = [] + for filepath in all_files: + with open(filepath, 'r') as f: + biblist.append(coder.decode_bibdata(f.read())) + + papers = {} + for b in biblist: + for k in b.entries: + try: + bibdata = BibliographyData() + bibdata.entries[k] = b.entries[k] + + papers[k] = Paper(bibdata, citekey=k) + except ValueError, e: + papers[k] = e + return papers + + def command(args): """ - :param bibpath: path (no url yet) to a bibliography file + :param bibpath: path (no url yet) to a bibliography file """ ui = get_ui() @@ -32,17 +76,28 @@ def command(args): copy = config().import_copy rp = repo.Repository(config()) # Extract papers from bib - papers = Paper.many_from_path(bibpath) + papers = many_from_path(bibpath) keys = args.keys or papers.keys() for k in keys: try: p = papers[k] if isinstance(p, Exception): - ui.error('Could not load entry for citekey {}.'.format(k)) + ui.error('could not load entry for citekey {}.'.format(k)) else: - doc_file = extract_doc_path_from_bibdata(p) - if doc_file is None: - ui.warning("No file for %s." % p.citekey) - add_paper_with_docfile(rp, p, docfile=doc_file, copy=copy) + docfile = bibstruct.extract_docfile(p.bibdata) + if docfile is None: + ui.warning("no file for {}.".format(p.citekey)) + else: + copy_doc = args.copy + if copy_doc is None: + copy_doc = config().import_copy + if copy_doc: + docfile = rp.databroker.copy_doc(p.citekey, docfile) + + p.docpath = docfile + rp.push_paper(p) + ui.print_('{} imported'.format(color.dye(p.citekey, color.cyan))) except KeyError: - ui.error('No entry found for citekey {}.'.format(k)) + ui.error('no entry found for citekey {}.'.format(k)) + except IOError, e: + ui.error(e.message) diff --git a/papers/commands/rename_cmd.py b/papers/commands/rename_cmd.py new file mode 100644 index 0000000..0540544 --- /dev/null +++ b/papers/commands/rename_cmd.py @@ -0,0 +1,27 @@ +from ..uis import get_ui +from ..configs import config +from .. import bibstruct +from .. import content +from .. import repo +from .. import paper + +def parser(subparsers): + parser = subparsers.add_parser('rename', help='rename the citekey of a repository') + parser.add_argument('citekey', + help='current citekey') + parser.add_argument('new_citekey', + help='new citekey') + return parser + + +def command(args): + """ + :param bibfile: bibtex file (in .bib, .bibml or .yaml format. + :param docfile: path (no url yet) to a pdf or ps file + """ + + ui = get_ui() + rp = repo.Repository(config()) + + paper = rp.pull_paper(args.citekey) + rp.rename_paper(paper, args.new_citekey) diff --git a/papers/databroker.py b/papers/databroker.py index a69f988..5679d53 100644 --- a/papers/databroker.py +++ b/papers/databroker.py @@ -53,7 +53,7 @@ class DataBroker(object): # docbroker def is_pubsdir_doc(self, docpath): - return self.docbroker.is_pusdir_doc(docpath) + return self.docbroker.is_pubsdir_doc(docpath) def copy_doc(self, citekey, source_path, overwrite=False): return self.docbroker.copy_doc(citekey, source_path, overwrite=overwrite) diff --git a/papers/datacache.py b/papers/datacache.py index 9859699..9d7a1f6 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -63,7 +63,7 @@ class DataCache(object): # docbroker def is_pubsdir_doc(self, docpath): - return self.databroker.is_pusdir_doc(docpath) + return self.databroker.is_pubsdir_doc(docpath) def copy_doc(self, citekey, source_path, overwrite=False): return self.databroker.copy_doc(citekey, source_path, overwrite=overwrite) diff --git a/papers/endecoder.py b/papers/endecoder.py index 2febb27..546141e 100644 --- a/papers/endecoder.py +++ b/papers/endecoder.py @@ -34,10 +34,12 @@ class EnDecoder(object): decode_fmt = {'bibyaml' : pybtex.database.input.bibyaml, 'bibtex' : pybtex.database.input.bibtex, + 'bib' : pybtex.database.input.bibtex, 'bibtexml': pybtex.database.input.bibtexml} encode_fmt = {'bibyaml' : pybtex.database.output.bibyaml, 'bibtex' : pybtex.database.output.bibtex, + 'bib' : pybtex.database.output.bibtex, 'bibtexml': pybtex.database.output.bibtexml} def encode_metadata(self, metadata): diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index 9135371..a308f9d 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -15,14 +15,18 @@ from .__init__ import __version__ CORE_CMDS = collections.OrderedDict([ ('init', commands.init_cmd), ('add', commands.add_cmd), + ('rename', commands.rename_cmd), + ('remove', commands.remove_cmd), ('list', commands.list_cmd), + + ('attach', commands.attach_cmd), ('open', commands.open_cmd), - ('websearch', commands.websearch_cmd), - ('remove', commands.remove_cmd), ('tag', commands.tag_cmd), - ('attach', commands.attach_cmd), + ('export', commands.export_cmd), - # ('import', commands.import_cmd), + ('import', commands.import_cmd), + + ('websearch', commands.websearch_cmd), # ('edit', commands.edit_cmd), # ('update', commands.update_cmd), ]) diff --git a/papers/repo.py b/papers/repo.py index 557968d..e717433 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -2,6 +2,8 @@ import shutil import glob import itertools +from pybtex.database import BibliographyData + from . import bibstruct from . import events from . import datacache @@ -65,7 +67,7 @@ class Repository(object): """ bibstruct.check_citekey(paper.citekey) if (not overwrite) and self.databroker.exists(paper.citekey, both = False): - raise IOError('files using this the {} citekey already exists'.format(citekey)) + raise IOError('files using the {} citekey already exists'.format(paper.citekey)) if (not overwrite) and self.citekeys is not None and paper.citekey in self.citekeys: raise CiteKeyCollision('citekey {} already in use'.format(paper.citekey)) @@ -101,8 +103,11 @@ class Repository(object): # check if new_citekey does not exists if self.databroker.exists(new_citekey, both=False): raise IOError("can't rename paper to {}, conflicting files exists".format(new_citekey)) - # modify bibdata - raise NotImplementedError + # modify bibdata (__delitem__ not implementd by pybtex) + new_bibdata = BibliographyData() + new_bibdata.entries[new_citekey] = paper.bibdata.entries[old_citekey] + paper.bibdata = new_bibdata + # move doc file if necessary if self.databroker.is_pubsdir_doc(paper.docpath): new_docpath = self.databroker.copy_doc(new_citekey, paper.docpath) @@ -110,11 +115,12 @@ class Repository(object): paper.docpath = new_docpath # push_paper to new_citekey - self.databroker.push(new_citekey, paper.metadata) + paper.citekey = new_citekey + self.push_paper(paper, event=False) # remove_paper of old_citekey - self.databroker.remove(old_citekey) + self.remove_paper(old_citekey, event=False) # send event - RenameEvent(paper, old_citekey).send() + events.RenameEvent(paper, old_citekey).send() def unique_citekey(self, base_key): """Create a unique citekey for a given basekey.""" diff --git a/tests/fake_env.py b/tests/fake_env.py index 1acfcb3..a8f480c 100644 --- a/tests/fake_env.py +++ b/tests/fake_env.py @@ -140,13 +140,13 @@ class FakeInput(): input() raise IndexError """ - def __init__(self, module_list, inputs=None): + def __init__(self, inputs, module_list=tuple()): self.inputs = list(inputs) or [] self.module_list = module_list self._cursor = 0 def as_global(self): - for md in module_list: + for md in self.module_list: md.input = self md.editor_input = self # if mdname.endswith('files'): diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index 0730beb..37a0702 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -66,6 +66,22 @@ bibtexml_raw0 = """ """ +bibtex_external0 = """ +@techreport{Page99, + number = {1999-66}, + month = {November}, + author = {Lawrence Page and Sergey Brin and Rajeev Motwani and Terry Winograd}, + note = {Previous number = SIDL-WP-1999-0120}, + title = {The PageRank Citation Ranking: Bringing Order to the Web.}, + type = {Technical Report}, + publisher = {Stanford InfoLab}, + year = {1999}, +institution = {Stanford InfoLab}, + url = {http://ilpubs.stanford.edu:8090/422/}, + abstract = "The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation.", +} +""" + bibtex_raw0 = """ @techreport{ Page99, diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 060665b..34e321e 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -1,159 +1,31 @@ -import sys -import os -import shutil -import glob import unittest -import pkgutil import re +import os import testenv -import fake_filesystem -import fake_filesystem_shutil -import fake_filesystem_glob +import fake_env from papers import papers_cmd -from papers import color, files -from papers.p3 import io, input +from papers import color, content, filebroker, uis, beets_ui, p3 -import fixtures +import str_fixtures +from papers.commands import init_cmd # code for fake fs -real_os = os -real_open = open -real_shutil = shutil -real_glob = glob - -fake_os, fake_open, fake_shutil, fake_glob = None, None, None, None - - -def _mod_list(): - ml = [] - import papers - for importer, modname, ispkg in pkgutil.walk_packages( - path=papers.__path__, - prefix=papers.__name__ + '.', - onerror=lambda x: None): - # HACK to not load textnote - if not modname.startswith('papers.plugs.texnote'): - ml.append((modname, __import__(modname, fromlist='dummy'))) - return ml - -mod_list = _mod_list() - - -def _create_fake_fs(): - global fake_os, fake_open, fake_shutil, fake_glob - - fake_fs = fake_filesystem.FakeFilesystem() - fake_os = fake_filesystem.FakeOsModule(fake_fs) - fake_open = fake_filesystem.FakeFileOpen(fake_fs) - fake_shutil = fake_filesystem_shutil.FakeShutilModule(fake_fs) - fake_glob = fake_filesystem_glob.FakeGlobModule(fake_fs) - - fake_fs.CreateDirectory(fake_os.path.expanduser('~')) - - try: - __builtins__.open = fake_open - __builtins__.file = fake_open - except AttributeError: - __builtins__['open'] = fake_open - __builtins__['file'] = fake_open - - sys.modules['os'] = fake_os - sys.modules['shutil'] = fake_shutil - sys.modules['glob'] = fake_glob - - for mdname, md in mod_list: - md.os = fake_os - md.shutil = fake_shutil - md.open = fake_open - md.file = fake_open - - return fake_fs - - -def _copy_data(fs): - """Copy all the data directory into the fake fs""" - datadir = real_os.path.join(real_os.path.dirname(__file__), 'data') - for filename in real_os.listdir(datadir): - real_path = real_os.path.join(datadir, filename) - fake_path = fake_os.path.join('data', filename) - if real_os.path.isfile(real_path): - with real_open(real_path, 'r') as f: - fs.CreateFile(fake_path, contents=f.read()) - if real_os.path.isdir(real_path): - fs.CreateDirectory(fake_path) - - - # redirecting output - -def redirect(f): - def newf(*args, **kwargs): - old_stderr, old_stdout = sys.stderr, sys.stdout - stdout = io.StringIO() - stderr = io.StringIO() - sys.stdout, sys.stderr = stdout, stderr - try: - return f(*args, **kwargs), stdout, stderr - finally: - sys.stderr, sys.stdout = old_stderr, old_stdout - return newf - - -# Test helpers - -# automating input - -real_input = input - - -class FakeInput(): - """ Replace the input() command, and mock user input during tests - - Instanciate as : - input = FakeInput(['yes', 'no']) - then replace the input command in every module of the package : - input.as_global() - Then : - input() returns 'yes' - input() returns 'no' - input() raise IndexError - """ - - def __init__(self, inputs=None): - self.inputs = list(inputs) or [] - self._cursor = 0 - - def as_global(self): - for mdname, md in mod_list: - md.input = self - md.editor_input = self - # if mdname.endswith('files'): - # md.editor_input = self - - def add_input(self, inp): - self.inputs.append(inp) - - def __call__(self, *args, **kwargs): - inp = self.inputs[self._cursor] - self._cursor += 1 - return inp - - class TestFakeInput(unittest.TestCase): def test_input(self): - input = FakeInput(['yes', 'no']) + input = fake_env.FakeInput(['yes', 'no']) self.assertEqual(input(), 'yes') self.assertEqual(input(), 'no') with self.assertRaises(IndexError): input() def test_input2(self): - other_input = FakeInput(['yes', 'no']) + other_input = fake_env.FakeInput(['yes', 'no'], module_list=[color]) other_input.as_global() self.assertEqual(color.input(), 'yes') self.assertEqual(color.input(), 'no') @@ -161,10 +33,11 @@ class TestFakeInput(unittest.TestCase): color.input() def test_editor_input(self): - other_input = FakeInput(['yes', 'no']) + other_input = fake_env.FakeInput(['yes', 'no'], + module_list=[content, color]) other_input.as_global() - self.assertEqual(files.editor_input(), 'yes') - self.assertEqual(files.editor_input(), 'no') + self.assertEqual(content.editor_input(), 'yes') + self.assertEqual(content.editor_input(), 'no') with self.assertRaises(IndexError): color.input() @@ -173,7 +46,7 @@ class CommandTestCase(unittest.TestCase): """Abstract TestCase intializing the fake filesystem.""" def setUp(self): - self.fs = _create_fake_fs() + self.fs = fake_env.create_fake_fs([content, filebroker, init_cmd]) def execute_cmds(self, cmds, fs=None): """ Execute a list of commands, and capture their output @@ -189,10 +62,10 @@ class CommandTestCase(unittest.TestCase): for cmd in cmds: if hasattr(cmd, '__iter__'): if len(cmd) == 2: - input = FakeInput(cmd[1]) + input = fake_env.FakeInput(cmd[1], [content, uis, beets_ui, p3]) input.as_global() - _, stdout, stderr = redirect(papers_cmd.execute)(cmd[0].split()) + _, stdout, stderr = fake_env.redirect(papers_cmd.execute)(cmd[0].split()) if len(cmd) == 3: actual_out = color.undye(stdout.getvalue()) correct_out = color.undye(cmd[2]) @@ -200,12 +73,14 @@ class CommandTestCase(unittest.TestCase): else: assert type(cmd) == str - _, stdout, stderr = redirect(papers_cmd.execute)(cmd.split()) + _, stdout, stderr = fake_env.redirect(papers_cmd.execute)(cmd.split()) assert(stderr.getvalue() == '') outs.append(color.undye(stdout.getvalue())) return outs + def tearDown(self): + fake_env.unset_fake_fs([content, filebroker]) class DataCommandTestCase(CommandTestCase): """Abstract TestCase intializing the fake filesystem and @@ -214,7 +89,7 @@ class DataCommandTestCase(CommandTestCase): def setUp(self): CommandTestCase.setUp(self) - _copy_data(self.fs) + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'data'), 'data') # Actual tests @@ -222,10 +97,16 @@ class DataCommandTestCase(CommandTestCase): class TestInit(CommandTestCase): def test_init(self): - papers_cmd.execute('papers init -p paper_test2'.split()) - self.assertEqual(set(fake_os.listdir('/paper_test2/')), - {'bibdata', 'doc', 'meta', 'papers.yaml'}) + pubsdir = os.path.expanduser('~/papers_test2') + papers_cmd.execute('papers init -p {}'.format(pubsdir).split()) + self.assertEqual(set(self.fs['os'].listdir(pubsdir)), + {'bib', 'doc', 'meta'}) + def test_init2(self): + pubsdir = os.path.expanduser('~/.papers') + papers_cmd.execute('papers init'.split()) + self.assertEqual(set(self.fs['os'].listdir(pubsdir)), + {'bib', 'doc', 'meta'}) class TestAdd(DataCommandTestCase): @@ -240,7 +121,7 @@ class TestAdd(DataCommandTestCase): 'papers add -b /data/pagerank.bib -d /data/pagerank.pdf', ] self.execute_cmds(cmds) - self.assertEqual(set(fake_os.listdir('/not_default/doc')), {'Page99.pdf'}) + self.assertEqual(set(self.fs['os'].listdir('/not_default/doc')), {'Page99.pdf'}) class TestList(DataCommandTestCase): @@ -288,12 +169,12 @@ class TestUsecase(DataCommandTestCase): def test_first(self): correct = ['Initializing papers in /paper_first.\n', '', - '0: [Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n', + '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n', '', '', 'search network\n', - '0: [Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) search network\n', - 'search network\n'] + '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) search network\n' + ] cmds = ['papers init -p paper_first/', 'papers add -d data/pagerank.pdf -b data/pagerank.bib', @@ -302,7 +183,6 @@ class TestUsecase(DataCommandTestCase): 'papers tag Page99 network+search', 'papers tag Page99', 'papers tag search', - 'papers tag 0', ] self.assertEqual(correct, self.execute_cmds(cmds)) @@ -343,18 +223,18 @@ class TestUsecase(DataCommandTestCase): def test_editor_success(self): cmds = ['papers init', - ('papers add', [fixtures.pagerankbib]), + ('papers add', [str_fixtures.bibtex_external0]), ('papers remove Page99', ['y']), ] self.execute_cmds(cmds) def test_edit(self): - bib = fixtures.pagerankbib + bib = str_fixtures.bibtex_external0 bib1 = re.sub('year = \{1999\}', 'year = {2007}', bib) bib2 = re.sub('Lawrence Page', 'Lawrence Ridge', bib1) bib3 = re.sub('Page99', 'Ridge07', bib2) - line = '0: [Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n' + line = '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n' line1 = re.sub('1999', '2007', line) line2 = re.sub('L. Page', 'L. Ridge', line1) line3 = re.sub('Page99', 'Ridge07', line2) @@ -374,9 +254,9 @@ class TestUsecase(DataCommandTestCase): def test_export(self): cmds = ['papers init', - ('papers add', [fixtures.pagerankbib]), + ('papers add', [str_fixtures.bibtex_external0]), 'papers export Page99', - ('papers export Page99 -f bibtex', [], fixtures.pagerankbib_generated), + ('papers export Page99 -f bibtex', [], str_fixtures.bibtex_raw0), 'papers export Page99 -f bibyaml', ] From e0dab17dcde92ee6879eb8470032fea2a8f8cf65 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 21:49:53 +0100 Subject: [PATCH 22/48] added troublesome bibfile --- tests/zoo/incollections.bib | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/zoo/incollections.bib diff --git a/tests/zoo/incollections.bib b/tests/zoo/incollections.bib new file mode 100644 index 0000000..c78351e --- /dev/null +++ b/tests/zoo/incollections.bib @@ -0,0 +1,14 @@ +@incollection{ +year={2007}, +isbn={978-3-540-74957-8}, +booktitle={Machine Learning: ECML 2007}, +volume={4701}, +series={Lecture Notes in Computer Science}, +editor={Kok, JoostN. and Koronacki, Jacek and Mantaras, RaomonLopezde and Matwin, Stan and Mladenič, Dunja and Skowron, Andrzej}, +doi={10.1007/978-3-540-74958-5_70}, +title={Transfer Learning in Reinforcement Learning Problems Through Partial Policy Recycling}, +url={http://dx.doi.org/10.1007/978-3-540-74958-5_70}, +publisher={Springer Berlin Heidelberg}, +author={Ramon, Jan and Driessens, Kurt and Croonenborghs, Tom}, +pages={699-707} +} From 98adc8a75061f71b704b76e70b7d9f3fd1d8d463 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 10 Nov 2013 21:54:09 +0100 Subject: [PATCH 23/48] removed helpers file --- papers/commands/helpers.py | 70 -------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 papers/commands/helpers.py diff --git a/papers/commands/helpers.py b/papers/commands/helpers.py deleted file mode 100644 index 91db970..0000000 --- a/papers/commands/helpers.py +++ /dev/null @@ -1,70 +0,0 @@ -from .. import files -from .. import color -from .. import pretty -from ..repo import InvalidReference -from ..paper import NoDocumentFile -from ..uis import get_ui - - -def add_references_argument(parser, single=False): - if single: - parser.add_argument('reference', - help='reference to the paper (citekey or number)') - else: - parser.add_argument('references', nargs='*', - help="one or several reference to export (citekeysor numbers)") - - -def add_docfile_to_paper(repo, paper, docfile, copy=False): - if copy: - repo.import_document(paper.citekey, docfile) - else: - paper.set_external_document(docfile) - repo.add_or_update(paper) - - -def add_paper_with_docfile(repo, paper, docfile=None, copy=False): - repo.add_paper(paper) - if docfile is not None: - add_docfile_to_paper(repo, paper, docfile, copy=copy) - - -def extract_doc_path_from_bibdata(paper): - try: - file_path = paper.get_document_file_from_bibdata(remove=True) - if files.check_file(file_path): - return file_path - else: - ui = get_ui() - ui.warning("File does not exist for %s (%s)." - % (paper.citekey, file_path)) - except NoDocumentFile: - return None - - -def parse_reference(rp, ref): - try: - return rp.ref2citekey(ref) - except InvalidReference: - ui = get_ui() - ui.error("no paper with reference: %s." - % color.dye(ref, color.citekey)) - ui.exit(-1) - - -def parse_references(rp, refs): - citekeys = [parse_reference(rp, ref) for ref in refs] - return citekeys - -def paper_oneliner(p, n = 0, citekey_only = False): - if citekey_only: - return p.citekey - else: - bibdesc = pretty.bib_oneliner(p.bibentry) - return (u'{num:d}: [{citekey}] {descr} {tags}'.format( - num=int(n), - citekey=color.dye(p.citekey, color.purple), - descr=bibdesc, - tags=color.dye(' '.join(p.tags), - color.purple, bold=True), - )).encode('utf-8') From 10fd0f86d64ef005cdd0e37e4c46178d9515b4ce Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 11 Nov 2013 00:52:09 +0100 Subject: [PATCH 24/48] only one usecase failing --- tests/str_fixtures.py | 4 ++-- tests/test_usecase.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index 37a0702..ba8f32a 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -82,8 +82,7 @@ institution = {Stanford InfoLab}, } """ -bibtex_raw0 = """ -@techreport{ +bibtex_raw0 = """@techreport{ Page99, author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", publisher = "Stanford InfoLab", @@ -96,6 +95,7 @@ bibtex_raw0 = """ year = "1999", institution = "Stanford InfoLab" } + """ metadata_raw0 = """external-document: null diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 34e321e..5688304 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -10,7 +10,7 @@ from papers import color, content, filebroker, uis, beets_ui, p3 import str_fixtures -from papers.commands import init_cmd +from papers.commands import init_cmd, import_cmd # code for fake fs @@ -46,7 +46,7 @@ class CommandTestCase(unittest.TestCase): """Abstract TestCase intializing the fake filesystem.""" def setUp(self): - self.fs = fake_env.create_fake_fs([content, filebroker, init_cmd]) + self.fs = fake_env.create_fake_fs([content, filebroker, init_cmd, import_cmd]) def execute_cmds(self, cmds, fs=None): """ Execute a list of commands, and capture their output From f92430de33e56c1692286b2baea18a34be3acb78 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 11 Nov 2013 00:52:32 +0100 Subject: [PATCH 25/48] fix unicode bug --- papers/endecoder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/papers/endecoder.py b/papers/endecoder.py index 546141e..aeac06b 100644 --- a/papers/endecoder.py +++ b/papers/endecoder.py @@ -56,7 +56,8 @@ class EnDecoder(object): def decode_bibdata(self, bibdata_raw): """""" - bibdata_rawutf8 = unicode(bibdata_raw) + bibdata_rawutf8 = bibdata_raw +# bibdata_rawutf8 = unicode(bibdata_raw, 'utf8') # FIXME this doesn't work for fmt in EnDecoder.decode_fmt.values(): try: bibdata_stream = StringIO.StringIO(bibdata_rawutf8) From 1ad64d7859cdc5822db16c4f2cdbc6124f0ecaa8 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 11 Nov 2013 04:07:42 +0100 Subject: [PATCH 26/48] notes cmd --- papers/commands/__init__.py | 1 + papers/commands/note_cmd.py | 27 +++++++++++++++++++++++++++ papers/databroker.py | 18 ++++++++++++++---- papers/datacache.py | 15 ++++++++++++--- papers/filebroker.py | 30 ++++++++++++++++-------------- papers/papers_cmd.py | 1 + papers/repo.py | 2 +- tests/test_databroker.py | 4 ++-- tests/test_filebroker.py | 6 +++--- tests/test_usecase.py | 4 ++-- 10 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 papers/commands/note_cmd.py diff --git a/papers/commands/__init__.py b/papers/commands/__init__.py index f832ba3..bc524c7 100644 --- a/papers/commands/__init__.py +++ b/papers/commands/__init__.py @@ -8,6 +8,7 @@ import list_cmd import attach_cmd import open_cmd import tag_cmd +import note_cmd # bulk import export_cmd import import_cmd diff --git a/papers/commands/note_cmd.py b/papers/commands/note_cmd.py new file mode 100644 index 0000000..073c46d --- /dev/null +++ b/papers/commands/note_cmd.py @@ -0,0 +1,27 @@ +from .. import repo +from .. import content +from ..configs import config +from ..uis import get_ui + +def parser(subparsers): + parser = subparsers.add_parser('note', + help='edit the note attached to a paper') + parser.add_argument('citekey', + help='citekey of the paper') + return parser + + +def command(args): + """ + """ + + ui = get_ui() + + + rp = repo.Repository(config()) + if not rp.databroker.exists(args.citekey): + ui.error("citekey {} not found".format(args.citekey)) + ui.exit(1) + + notepath = rp.databroker.real_notepath('notesdir://{}.txt'.format(args.citekey)) + content.edit_file(config().edit_cmd, notepath, temporary=False) diff --git a/papers/databroker.py b/papers/databroker.py index 5679d53..e5c5680 100644 --- a/papers/databroker.py +++ b/papers/databroker.py @@ -12,7 +12,8 @@ class DataBroker(object): def __init__(self, directory, create=False): self.filebroker = filebroker.FileBroker(directory, create=create) self.endecoder = endecoder.EnDecoder() - self.docbroker = filebroker.DocBroker(directory) + self.docbroker = filebroker.DocBroker(directory, scheme='docsdir', subdir='doc') + self.notebroker = filebroker.DocBroker(directory, scheme='notesdir', subdir='notes') # filebroker+endecoder @@ -52,8 +53,8 @@ class DataBroker(object): # docbroker - def is_pubsdir_doc(self, docpath): - return self.docbroker.is_pubsdir_doc(docpath) + def in_docsdir(self, docpath): + return self.docbroker.in_docsdir(docpath) def copy_doc(self, citekey, source_path, overwrite=False): return self.docbroker.copy_doc(citekey, source_path, overwrite=overwrite) @@ -62,4 +63,13 @@ class DataBroker(object): return self.docbroker.remove_doc(docpath, silent=silent) def real_docpath(self, docpath): - return self.docbroker.real_docpath(docpath) \ No newline at end of file + return self.docbroker.real_docpath(docpath) + + + # notesbroker + + def in_notesdir(self, docpath): + return self.notebroker.in_docsdir(docpath) + + def real_notepath(self, docpath): + return self.notebroker.real_docpath(docpath) \ No newline at end of file diff --git a/papers/datacache.py b/papers/datacache.py index 9d7a1f6..ef6646d 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -60,10 +60,10 @@ class DataCache(object): """Will return None if bibdata_raw can't be decoded""" return self.databroker.verify(bibdata_raw) - # docbroker + # docbroker - def is_pubsdir_doc(self, docpath): - return self.databroker.is_pubsdir_doc(docpath) + def in_docsdir(self, docpath): + return self.databroker.in_docsdir(docpath) def copy_doc(self, citekey, source_path, overwrite=False): return self.databroker.copy_doc(citekey, source_path, overwrite=overwrite) @@ -74,6 +74,15 @@ class DataCache(object): def real_docpath(self, docpath): return self.databroker.real_docpath(docpath) + # notesbroker + + def in_notesdir(self, docpath): + return self.databroker.in_notesdir(docpath) + + def real_notepath(self, docpath): + return self.databroker.real_notepath(docpath) + + # class ChangeTracker(object): # def __init__(self, cache, directory): diff --git a/papers/filebroker.py b/papers/filebroker.py index aa43e74..1b257ce 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -107,38 +107,37 @@ class DocBroker(object): * only one document can be attached to a paper (might change in the future) * this document can be anything, the content is never processed. - * these document have an adress of the type "pubsdir://doc/citekey.pdf" + * these document have an adress of the type "docsdir://citekey.pdf" + * docsdir:// correspond to /path/to/pubsdir/doc (configurable) * document outside of the repository will not be removed. * deliberately, there is no move_doc method. """ - def __init__(self, directory): - self.docdir = os.path.join(directory, 'doc') + def __init__(self, directory, scheme='docsdir', subdir='doc'): + self.scheme = scheme + self.docdir = os.path.join(directory, subdir) if not check_directory(self.docdir, fail = False): os.mkdir(self.docdir) - def is_pubsdir_doc(self, docpath): + def in_docsdir(self, docpath): try: parsed = urlparse.urlparse(docpath) except Exception: return False - if parsed.scheme == 'pubsdir': - assert parsed.netloc == 'doc' - assert parsed.path[0] == '/' - return parsed.scheme == 'pubsdir' + return parsed.scheme == self.scheme def copy_doc(self, citekey, source_path, overwrite=False): """ Copy a document to the pubsdir/doc, and return the location The document will be named {citekey}.{ext}. - The location will be pubsdir://doc/{citekey}.{ext}. + The location will be docsdir://{citekey}.{ext}. :param overwrite: will overwrite existing file. :return: the above location """ full_source_path = self.real_docpath(source_path) check_file(full_source_path) - target_path = 'pubsdir://' + os.path.join('doc', citekey + os.path.splitext(source_path)[-1]) + target_path = '{}://{}'.format(self.scheme, citekey + os.path.splitext(source_path)[-1]) full_target_path = self.real_docpath(target_path) if not overwrite and check_file(full_target_path, fail=False): raise IOError('{} file exists.'.format(full_target_path)) @@ -147,11 +146,11 @@ class DocBroker(object): return target_path def remove_doc(self, docpath, silent=True): - """ Will remove only file hosted in pubsdir://doc/ + """ Will remove only file hosted in docsdir:// :raise ValueError: for other paths, unless :param silent: is True """ - if not self.is_pubsdir_doc(docpath): + if not self.in_docsdir(docpath): if not silent: raise ValueError(('the file to be removed {} is set as external. ' 'you should remove it manually.').format(docpath)) @@ -165,7 +164,10 @@ class DocBroker(object): Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}. Return absoluted paths of regular ones otherwise. """ - if self.is_pubsdir_doc(docpath): + if self.in_docsdir(docpath): parsed = urlparse.urlparse(docpath) - docpath = os.path.join(self.docdir, parsed.path[1:]) + if parsed.path == '': + docpath = os.path.join(self.docdir, parsed.netloc) + else: + docpath = os.path.join(self.docdir, parsed.netloc, parsed.path[1:]) return os.path.normpath(os.path.abspath(docpath)) diff --git a/papers/papers_cmd.py b/papers/papers_cmd.py index a308f9d..0433fc5 100644 --- a/papers/papers_cmd.py +++ b/papers/papers_cmd.py @@ -22,6 +22,7 @@ CORE_CMDS = collections.OrderedDict([ ('attach', commands.attach_cmd), ('open', commands.open_cmd), ('tag', commands.tag_cmd), + ('note', commands.note_cmd), ('export', commands.export_cmd), ('import', commands.import_cmd), diff --git a/papers/repo.py b/papers/repo.py index e717433..7469d66 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -109,7 +109,7 @@ class Repository(object): paper.bibdata = new_bibdata # move doc file if necessary - if self.databroker.is_pubsdir_doc(paper.docpath): + if self.databroker.in_docsdir(paper.docpath): new_docpath = self.databroker.copy_doc(new_citekey, paper.docpath) self.databroker.remove_doc(paper.docpath) paper.docpath = new_docpath diff --git a/tests/test_databroker.py b/tests/test_databroker.py index 255585b..7e4971a 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -67,8 +67,8 @@ class TestDataBroker(TestFakeFs): with self.assertRaises(IOError): db.pull_metadata('citekey') - db.copy_doc('Larry99', 'pubsdir://doc/Page99.pdf') + db.copy_doc('Larry99', 'docsdir://Page99.pdf') self.assertTrue(content.check_file('repo/doc/Page99.pdf', fail=False)) self.assertTrue(content.check_file('repo/doc/Larry99.pdf', fail=False)) - db.remove_doc('pubsdir://doc/Page99.pdf') + db.remove_doc('docsdir://Page99.pdf') diff --git a/tests/test_filebroker.py b/tests/test_filebroker.py index 2691dad..32833a8 100644 --- a/tests/test_filebroker.py +++ b/tests/test_filebroker.py @@ -101,9 +101,9 @@ class TestDocBroker(TestFakeFs): docpath = docb.copy_doc('Page99', 'data/pagerank.pdf') self.assertTrue(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'))) - self.assertTrue(docb.is_pubsdir_doc(docpath)) - self.assertEqual(docpath, 'pubsdir://doc/Page99.pdf') - docb.remove_doc('pubsdir://doc/Page99.pdf') + self.assertTrue(docb.in_docsdir(docpath)) + self.assertEqual(docpath, 'docsdir://Page99.pdf') + docb.remove_doc('docsdir://Page99.pdf') self.assertFalse(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'), fail=False)) with self.assertRaises(IOError): diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 5688304..b6c7c77 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -100,13 +100,13 @@ class TestInit(CommandTestCase): pubsdir = os.path.expanduser('~/papers_test2') papers_cmd.execute('papers init -p {}'.format(pubsdir).split()) self.assertEqual(set(self.fs['os'].listdir(pubsdir)), - {'bib', 'doc', 'meta'}) + {'bib', 'doc', 'meta', 'notes'}) def test_init2(self): pubsdir = os.path.expanduser('~/.papers') papers_cmd.execute('papers init'.split()) self.assertEqual(set(self.fs['os'].listdir(pubsdir)), - {'bib', 'doc', 'meta'}) + {'bib', 'doc', 'meta', 'notes'}) class TestAdd(DataCommandTestCase): From 29897cc0ad95b6caa81cbcf6c56532be9dcbc079 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 11 Nov 2013 15:24:09 +0100 Subject: [PATCH 27/48] rename also rename notes --- papers/databroker.py | 10 ++++++++-- papers/datacache.py | 6 ++++++ papers/filebroker.py | 3 +++ papers/repo.py | 8 ++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/papers/databroker.py b/papers/databroker.py index e5c5680..7ffa6ee 100644 --- a/papers/databroker.py +++ b/papers/databroker.py @@ -35,7 +35,7 @@ class DataBroker(object): def push(self, citekey, metadata, bibdata): self.filebroker.push(citekey, metadata, bibdata) - + def remove(self, citekey): self.filebroker.remove(citekey) @@ -65,11 +65,17 @@ class DataBroker(object): def real_docpath(self, docpath): return self.docbroker.real_docpath(docpath) - # notesbroker def in_notesdir(self, docpath): return self.notebroker.in_docsdir(docpath) + def copy_note(self, citekey, source_path, overwrite=False): + return self.notebroker.copy_doc(citekey, source_path, overwrite=overwrite) + + def remove_note(self, docpath, silent=True): + return self.notebroker.remove_doc(docpath, silent=silent) + + def real_notepath(self, docpath): return self.notebroker.real_docpath(docpath) \ No newline at end of file diff --git a/papers/datacache.py b/papers/datacache.py index ef6646d..636d8e5 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -79,6 +79,12 @@ class DataCache(object): def in_notesdir(self, docpath): return self.databroker.in_notesdir(docpath) + def copy_note(self, citekey, source_path, overwrite=False): + return self.databroker.copy_note(citekey, source_path, overwrite=overwrite) + + def remove_note(self, docpath, silent=True): + return self.databroker.remove_note(docpath, silent=silent) + def real_notepath(self, docpath): return self.databroker.real_notepath(docpath) diff --git a/papers/filebroker.py b/papers/filebroker.py index 1b257ce..c721950 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -126,6 +126,9 @@ class DocBroker(object): return False return parsed.scheme == self.scheme + # def doc_exists(self, citekey, ext='.txt'): + # return check_file(os.path.join(self.docdir, citekey + ext), fail=False) + def copy_doc(self, citekey, source_path, overwrite=False): """ Copy a document to the pubsdir/doc, and return the location diff --git a/papers/repo.py b/papers/repo.py index 7469d66..21b7a2c 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -114,6 +114,14 @@ class Repository(object): self.databroker.remove_doc(paper.docpath) paper.docpath = new_docpath + try: + old_notepath = 'notesdir://{}.txt'.format(old_citekey) + new_notepath = self.databroker.copy_note(new_citekey, old_notepath) + self.databroker.remove_notei(old_notepath) + except IOError: + import traceback + traceback.print_exc() + # push_paper to new_citekey paper.citekey = new_citekey self.push_paper(paper, event=False) From 148917c70c8257e9a6fa31a0cd56d06d03afbb2b Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Tue, 12 Nov 2013 13:34:39 +0100 Subject: [PATCH 28/48] move_doc in docbroker + consequences + remove_cmd remove notes too --- papers/commands/note_cmd.py | 2 +- papers/databroker.py | 29 ++++++++++++++----------- papers/datacache.py | 24 ++++++++++----------- papers/filebroker.py | 43 +++++++++++++++++++++++++------------ papers/repo.py | 14 ++++++------ 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/papers/commands/note_cmd.py b/papers/commands/note_cmd.py index 073c46d..ad2a3eb 100644 --- a/papers/commands/note_cmd.py +++ b/papers/commands/note_cmd.py @@ -23,5 +23,5 @@ def command(args): ui.error("citekey {} not found".format(args.citekey)) ui.exit(1) - notepath = rp.databroker.real_notepath('notesdir://{}.txt'.format(args.citekey)) + notepath = rp.databroker.real_notepath(args.citekey) content.edit_file(config().edit_cmd, notepath, temporary=False) diff --git a/papers/databroker.py b/papers/databroker.py index 7ffa6ee..7614a43 100644 --- a/papers/databroker.py +++ b/papers/databroker.py @@ -56,26 +56,29 @@ class DataBroker(object): def in_docsdir(self, docpath): return self.docbroker.in_docsdir(docpath) - def copy_doc(self, citekey, source_path, overwrite=False): - return self.docbroker.copy_doc(citekey, source_path, overwrite=overwrite) + def real_docpath(self, docpath): + return self.docbroker.real_docpath(docpath) + + def add_doc(self, citekey, source_path, overwrite=False): + return self.docbroker.add_doc(citekey, source_path, overwrite=overwrite) def remove_doc(self, docpath, silent=True): return self.docbroker.remove_doc(docpath, silent=silent) - def real_docpath(self, docpath): - return self.docbroker.real_docpath(docpath) + def rename_doc(self, docpath, new_citekey): + return self.docbroker.rename_doc(docpath, new_citekey) # notesbroker - def in_notesdir(self, docpath): - return self.notebroker.in_docsdir(docpath) - - def copy_note(self, citekey, source_path, overwrite=False): - return self.notebroker.copy_doc(citekey, source_path, overwrite=overwrite) + def real_notepath(self, citekey): + notepath = 'notesdir://{}.txt'.format(citekey) + return self.notebroker.real_docpath(notepath) - def remove_note(self, docpath, silent=True): - return self.notebroker.remove_doc(docpath, silent=silent) + def remove_note(self, citekey, silent=True): + notepath = 'notesdir://{}.txt'.format(citekey) + return self.notebroker.remove_doc(notepath, silent=silent) + def rename_note(self, old_citekey, new_citekey): + notepath = 'notesdir://{}.txt'.format(old_citekey) + return self.notebroker.rename_doc(notepath, new_citekey) - def real_notepath(self, docpath): - return self.notebroker.real_docpath(docpath) \ No newline at end of file diff --git a/papers/datacache.py b/papers/datacache.py index 636d8e5..8dbec1a 100644 --- a/papers/datacache.py +++ b/papers/datacache.py @@ -65,28 +65,28 @@ class DataCache(object): def in_docsdir(self, docpath): return self.databroker.in_docsdir(docpath) + def real_docpath(self, docpath): + return self.databroker.real_docpath(docpath) + def copy_doc(self, citekey, source_path, overwrite=False): - return self.databroker.copy_doc(citekey, source_path, overwrite=overwrite) + return self.databroker.add_doc(citekey, source_path, overwrite=overwrite) def remove_doc(self, docpath, silent=True): return self.databroker.remove_doc(docpath, silent=silent) - def real_docpath(self, docpath): - return self.databroker.real_docpath(docpath) + def rename_doc(self, docpath, new_citekey): + return self.databroker.rename_doc(docpath, new_citekey) # notesbroker - def in_notesdir(self, docpath): - return self.databroker.in_notesdir(docpath) - - def copy_note(self, citekey, source_path, overwrite=False): - return self.databroker.copy_note(citekey, source_path, overwrite=overwrite) + def real_notepath(self, citekey): + return self.databroker.real_notepath(citekey) - def remove_note(self, docpath, silent=True): - return self.databroker.remove_note(docpath, silent=silent) + def remove_note(self, citekey, silent=True): + return self.databroker.remove_note(citekey, silent=True) - def real_notepath(self, docpath): - return self.databroker.real_notepath(docpath) + def rename_note(self, old_citekey, new_citekey): + return self.databroker.rename_note(old_citekey, new_citekey) # class ChangeTracker(object): diff --git a/papers/filebroker.py b/papers/filebroker.py index c721950..a723ab9 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -110,7 +110,7 @@ class DocBroker(object): * these document have an adress of the type "docsdir://citekey.pdf" * docsdir:// correspond to /path/to/pubsdir/doc (configurable) * document outside of the repository will not be removed. - * deliberately, there is no move_doc method. + * move_doc only applies from inside to inside the docsdir """ def __init__(self, directory, scheme='docsdir', subdir='doc'): @@ -129,8 +129,21 @@ class DocBroker(object): # def doc_exists(self, citekey, ext='.txt'): # return check_file(os.path.join(self.docdir, citekey + ext), fail=False) - def copy_doc(self, citekey, source_path, overwrite=False): - """ Copy a document to the pubsdir/doc, and return the location + def real_docpath(self, docpath): + """Return the full path + Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}. + Return absoluted paths of regular ones otherwise. + """ + if self.in_docsdir(docpath): + parsed = urlparse.urlparse(docpath) + if parsed.path == '': + docpath = os.path.join(self.docdir, parsed.netloc) + else: + docpath = os.path.join(self.docdir, parsed.netloc, parsed.path[1:]) + return os.path.normpath(os.path.abspath(docpath)) + + def add_doc(self, citekey, source_path, overwrite=False): + """ Add a document to the docsdir, and return its location. The document will be named {citekey}.{ext}. The location will be docsdir://{citekey}.{ext}. @@ -162,15 +175,17 @@ class DocBroker(object): if check_file(filepath): os.remove(filepath) - def real_docpath(self, docpath): - """Return the full path - Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}. - Return absoluted paths of regular ones otherwise. + def rename_doc(self, docpath, new_citekey): + """ Move a document inside the docsdir + + :raise IOError: if docpath doesn't point to a file + if new_citekey doc exists already. + :raise ValueError: if docpath is not in docsdir(). + + if an exception is raised, the files on disk haven't changed. """ - if self.in_docsdir(docpath): - parsed = urlparse.urlparse(docpath) - if parsed.path == '': - docpath = os.path.join(self.docdir, parsed.netloc) - else: - docpath = os.path.join(self.docdir, parsed.netloc, parsed.path[1:]) - return os.path.normpath(os.path.abspath(docpath)) + if not self.in_docsdir(docpath): + raise ValueError('cannot rename an external file ({}).'.format(docpath)) + + new_notepath = self.add_doc(new_citekey, docpath) + self.remove_doc(docpath) \ No newline at end of file diff --git a/papers/repo.py b/papers/repo.py index 21b7a2c..df474ba 100644 --- a/papers/repo.py +++ b/papers/repo.py @@ -87,6 +87,7 @@ class Repository(object): metadata = self.databroker.pull_metadata(citekey) docpath = metadata.get('docfile') self.databroker.remove_doc(docpath, silent=True) + self.databroker.remove_note(citekey, silent=True) except IOError: pass # FXME: if IOError is about being unable to # remove the file, we need to issue an error.I @@ -103,6 +104,7 @@ class Repository(object): # check if new_citekey does not exists if self.databroker.exists(new_citekey, both=False): raise IOError("can't rename paper to {}, conflicting files exists".format(new_citekey)) + # modify bibdata (__delitem__ not implementd by pybtex) new_bibdata = BibliographyData() new_bibdata.entries[new_citekey] = paper.bibdata.entries[old_citekey] @@ -110,17 +112,13 @@ class Repository(object): # move doc file if necessary if self.databroker.in_docsdir(paper.docpath): - new_docpath = self.databroker.copy_doc(new_citekey, paper.docpath) - self.databroker.remove_doc(paper.docpath) - paper.docpath = new_docpath + paper.docpath = self.databroker.rename_doc(paper.docpath, new_citekey) + # move note file if necessary try: - old_notepath = 'notesdir://{}.txt'.format(old_citekey) - new_notepath = self.databroker.copy_note(new_citekey, old_notepath) - self.databroker.remove_notei(old_notepath) + self.databroker.rename_note(old_citekey, new_citekey) except IOError: - import traceback - traceback.print_exc() + pass # push_paper to new_citekey paper.citekey = new_citekey From b03b899c5a207ac6c346642ee87e129f04ab8cf4 Mon Sep 17 00:00:00 2001 From: humm Date: Wed, 13 Nov 2013 22:06:41 +0100 Subject: [PATCH 29/48] added support of url (as long as they begin in http://) for docfiles --- papers/bibstruct.py | 25 ++++++++++++++----------- papers/commands/add_cmd.py | 2 +- papers/content.py | 33 ++++++++++++++++++++++++++++++++- papers/filebroker.py | 10 ++++++++-- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/papers/bibstruct.py b/papers/bibstruct.py index 8b9f317..6b05ce6 100644 --- a/papers/bibstruct.py +++ b/papers/bibstruct.py @@ -67,16 +67,19 @@ def extract_docfile(bibdata, remove=False): citekey, entry = get_entry(bibdata) try: - field = entry.fields['file'] - # Check if this is mendeley specific - for f in field.split(':'): - if len(f) > 0: - break - if remove: - entry.fields.pop('file') - # This is a hck for Mendeley. Make clean - if f[0] != '/': - f = '/' + f - return f + if 'file' in entry.fields: + field = entry.fields['file'] + # Check if this is mendeley specific + for f in field.split(':'): + if len(f) > 0: + break + if remove: + entry.fields.pop('file') + # This is a hck for Mendeley. Make clean + if f[0] != '/': + f = '/' + f + return f + if 'attachments' in entry.fields: + return entry.fields['attachments'] except (KeyError, IndexError): return None diff --git a/papers/commands/add_cmd.py b/papers/commands/add_cmd.py index f35301d..6b80e83 100644 --- a/papers/commands/add_cmd.py +++ b/papers/commands/add_cmd.py @@ -89,7 +89,7 @@ def command(args): if copy_doc is None: copy_doc = config().import_copy if copy_doc: - docfile = rp.databroker.copy_doc(citekey, docfile) + docfile = rp.databroker.copy_doc(citekey, docfile) # create the paper diff --git a/papers/content.py b/papers/content.py index 737653b..113614b 100644 --- a/papers/content.py +++ b/papers/content.py @@ -1,6 +1,12 @@ import os import subprocess import tempfile +import shutil + +import urlparse +import httplib +import urllib2 + # files i/o @@ -38,9 +44,34 @@ def write_file(filepath, data): # dealing with formatless content +def content_type(path): + parsed = urlparse.urlparse(path) + if parsed.scheme == 'http': + return 'url' + else: + return 'file' + +def url_exists(url): + parsed = urlparse.urlparse(url) + conn = httplib.HTTPConnection(parsed.netloc) + conn.request('HEAD', parsed.path) + response = conn.getresponse() + conn.close() + return response.status == 200 + + +def check_content(path): + if content_type(path) == 'url': + return url_exists(path) + else: + return check_file(path) + def get_content(path): """Will be useful when we need to get content from url""" - return read_file(path) + if content_type(path) == 'url': + return urllib2.urlopen(path) + else: + return read_file(path) def move_content(source, target, overwrite = False): if source == target: diff --git a/papers/filebroker.py b/papers/filebroker.py index a723ab9..665e1cf 100644 --- a/papers/filebroker.py +++ b/papers/filebroker.py @@ -4,6 +4,7 @@ import re import urlparse from .content import check_file, check_directory, read_file, write_file +from .content import check_content, content_type, get_content def filter_filename(filename, ext): """ Return the filename without the extension if the extension matches ext. @@ -140,6 +141,8 @@ class DocBroker(object): docpath = os.path.join(self.docdir, parsed.netloc) else: docpath = os.path.join(self.docdir, parsed.netloc, parsed.path[1:]) + elif content_type(docpath) != 'file': + return docpath return os.path.normpath(os.path.abspath(docpath)) def add_doc(self, citekey, source_path, overwrite=False): @@ -151,13 +154,16 @@ class DocBroker(object): :return: the above location """ full_source_path = self.real_docpath(source_path) - check_file(full_source_path) + check_content(full_source_path) target_path = '{}://{}'.format(self.scheme, citekey + os.path.splitext(source_path)[-1]) full_target_path = self.real_docpath(target_path) if not overwrite and check_file(full_target_path, fail=False): raise IOError('{} file exists.'.format(full_target_path)) - shutil.copy(full_source_path, full_target_path) + + doc_content = get_content(full_source_path) + with open(full_target_path, 'wb') as f: + f.write(doc_content.read()) return target_path From 8d915454727b0924876e045e4c166ee91995d1e5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Thu, 14 Nov 2013 04:40:29 +0100 Subject: [PATCH 30/48] papers renamed 'pubs' --- .gitignore | 2 +- papers/commands/update_cmd.py | 63 ------- papers/papers | 5 - {papers => pubs}/__init__.py | 0 {papers => pubs}/beets_ui.py | 0 {papers => pubs}/bibstruct.py | 0 {papers => pubs}/color.py | 0 {papers => pubs}/commands/__init__.py | 0 {papers => pubs}/commands/add_cmd.py | 0 {papers => pubs}/commands/attach_cmd.py | 0 {papers => pubs}/commands/edit_cmd.py | 19 +- {papers => pubs}/commands/export_cmd.py | 0 {papers => pubs}/commands/import_cmd.py | 0 {papers => pubs}/commands/init_cmd.py | 10 +- {papers => pubs}/commands/list_cmd.py | 0 {papers => pubs}/commands/note_cmd.py | 0 {papers => pubs}/commands/open_cmd.py | 0 {papers => pubs}/commands/remove_cmd.py | 0 {papers => pubs}/commands/rename_cmd.py | 0 {papers => pubs}/commands/tag_cmd.py | 14 +- pubs/commands/update_cmd.py | 37 ++++ {papers => pubs}/commands/websearch_cmd.py | 0 {papers => pubs}/configs.py | 6 +- {papers => pubs}/content.py | 0 {papers => pubs}/databroker.py | 0 {papers => pubs}/datacache.py | 0 {papers => pubs}/endecoder.py | 0 {papers => pubs}/events.py | 0 {papers => pubs}/filebroker.py | 0 {papers => pubs}/p3.py | 0 {papers => pubs}/paper.py | 0 {papers => pubs}/plugins.py | 2 +- {papers => pubs}/plugs/__init__.py | 0 {papers => pubs}/plugs/alias/__init__.py | 0 {papers => pubs}/plugs/alias/alias.py | 2 +- {papers => pubs}/plugs/texnote/__init__.py | 0 .../plugs/texnote/autofill_tools.py | 0 .../plugs/texnote/default_body.tex | 0 .../plugs/texnote/default_style.sty | 0 {papers => pubs}/plugs/texnote/latex_tools.py | 0 {papers => pubs}/plugs/texnote/texnote.py | 0 {papers => pubs}/pretty.py | 0 pubs/pubs | 5 + papers/papers_cmd.py => pubs/pubs_cmd.py | 8 +- {papers => pubs}/repo.py | 0 {papers => pubs}/uis.py | 0 setup.py | 4 +- tests/fake_env.py | 12 +- tests/fixtures.py | 38 +--- tests/test.sh | 16 +- tests/test_color.py | 2 +- tests/test_config.py | 16 +- tests/test_databroker.py | 4 +- tests/test_endecoder.py | 2 +- tests/test_events.py | 2 +- tests/test_filebroker.py | 2 +- tests/test_queries.py | 18 +- tests/test_repo.py | 8 +- tests/test_tag.py | 2 +- tests/test_usecase.py | 176 +++++++++--------- 60 files changed, 213 insertions(+), 262 deletions(-) delete mode 100644 papers/commands/update_cmd.py delete mode 100755 papers/papers rename {papers => pubs}/__init__.py (100%) rename {papers => pubs}/beets_ui.py (100%) rename {papers => pubs}/bibstruct.py (100%) rename {papers => pubs}/color.py (100%) rename {papers => pubs}/commands/__init__.py (100%) rename {papers => pubs}/commands/add_cmd.py (100%) rename {papers => pubs}/commands/attach_cmd.py (100%) rename {papers => pubs}/commands/edit_cmd.py (76%) rename {papers => pubs}/commands/export_cmd.py (100%) rename {papers => pubs}/commands/import_cmd.py (100%) rename {papers => pubs}/commands/init_cmd.py (79%) rename {papers => pubs}/commands/list_cmd.py (100%) rename {papers => pubs}/commands/note_cmd.py (100%) rename {papers => pubs}/commands/open_cmd.py (100%) rename {papers => pubs}/commands/remove_cmd.py (100%) rename {papers => pubs}/commands/rename_cmd.py (100%) rename {papers => pubs}/commands/tag_cmd.py (94%) create mode 100644 pubs/commands/update_cmd.py rename {papers => pubs}/commands/websearch_cmd.py (100%) rename {papers => pubs}/configs.py (94%) rename {papers => pubs}/content.py (100%) rename {papers => pubs}/databroker.py (100%) rename {papers => pubs}/datacache.py (100%) rename {papers => pubs}/endecoder.py (100%) rename {papers => pubs}/events.py (100%) rename {papers => pubs}/filebroker.py (100%) rename {papers => pubs}/p3.py (100%) rename {papers => pubs}/paper.py (100%) rename {papers => pubs}/plugins.py (95%) rename {papers => pubs}/plugs/__init__.py (100%) rename {papers => pubs}/plugs/alias/__init__.py (100%) rename {papers => pubs}/plugs/alias/alias.py (98%) rename {papers => pubs}/plugs/texnote/__init__.py (100%) rename {papers => pubs}/plugs/texnote/autofill_tools.py (100%) rename {papers => pubs}/plugs/texnote/default_body.tex (100%) rename {papers => pubs}/plugs/texnote/default_style.sty (100%) rename {papers => pubs}/plugs/texnote/latex_tools.py (100%) rename {papers => pubs}/plugs/texnote/texnote.py (100%) rename {papers => pubs}/pretty.py (100%) create mode 100755 pubs/pubs rename papers/papers_cmd.py => pubs/pubs_cmd.py (92%) rename {papers => pubs}/repo.py (100%) rename {papers => pubs}/uis.py (100%) diff --git a/.gitignore b/.gitignore index 114bd34..18d17b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ build/ dist/ *~ *.pyc -papers.egg-info +*.egg-info .DS_Store diff --git a/papers/commands/update_cmd.py b/papers/commands/update_cmd.py deleted file mode 100644 index e693090..0000000 --- a/papers/commands/update_cmd.py +++ /dev/null @@ -1,63 +0,0 @@ -import sys - -from .. import repo -from .. import color -from ..configs import config -from ..uis import get_ui -from ..__init__ import __version__ - -def parser(subparsers): - parser = subparsers.add_parser('update', help='update the repository to the lastest format') - return parser - - -def command(args): - - ui = get_ui() - - code_version = __version__ - repo_version = int(config().version) - - if repo_version == code_version: - ui.print_('You papers repository is up-to-date.') - sys.exit(0) - elif repo_version > code_version: - ui.print_('Your repository was generated with an newer version of papers.\n' - 'You should not use papers until you install the newest version.') - sys.exit(0) - else: - msg = ("You should backup the paper directory {} before continuing." - "Continue ?").format(color.dye(config().papers_dir, color.filepath)) - sure = ui.input_yn(question=msg, default='n') - if not sure: - sys.exit(0) - - if repo_version == 1: - rp = repo.Repository(config()) - for p in rp.all_papers(): - tags = set(p.metadata['tags']) - tags = tags.union(p.metadata['labels']) - p.metadata.pop('labels', []) - rp.save_paper(p) - repo_version = 2 - - - if repo_version == 2: - # update config - cfg_update = [('papers-directory', 'papers_dir'), - ('open-cmd', 'open_cmd'), - ('edit-cmd', 'edit_cmd'), - ('import-copy', 'import_copy'), - ('import-move', 'import_move'), - ] - for old, new in cfg_update: - try: - config()._cfg.set('papers', new, config()._cfg.get('papers', old)) - config()._cfg.remove_option('papers', old) - except Exception: - pass - config().save() - repo_version = 3 - - config().version = repo_version - config().save() diff --git a/papers/papers b/papers/papers deleted file mode 100755 index 1cc68db..0000000 --- a/papers/papers +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding:utf-8 -*- - -from papers import papers_cmd -papers_cmd.execute() \ No newline at end of file diff --git a/papers/__init__.py b/pubs/__init__.py similarity index 100% rename from papers/__init__.py rename to pubs/__init__.py diff --git a/papers/beets_ui.py b/pubs/beets_ui.py similarity index 100% rename from papers/beets_ui.py rename to pubs/beets_ui.py diff --git a/papers/bibstruct.py b/pubs/bibstruct.py similarity index 100% rename from papers/bibstruct.py rename to pubs/bibstruct.py diff --git a/papers/color.py b/pubs/color.py similarity index 100% rename from papers/color.py rename to pubs/color.py diff --git a/papers/commands/__init__.py b/pubs/commands/__init__.py similarity index 100% rename from papers/commands/__init__.py rename to pubs/commands/__init__.py diff --git a/papers/commands/add_cmd.py b/pubs/commands/add_cmd.py similarity index 100% rename from papers/commands/add_cmd.py rename to pubs/commands/add_cmd.py diff --git a/papers/commands/attach_cmd.py b/pubs/commands/attach_cmd.py similarity index 100% rename from papers/commands/attach_cmd.py rename to pubs/commands/attach_cmd.py diff --git a/papers/commands/edit_cmd.py b/pubs/commands/edit_cmd.py similarity index 76% rename from papers/commands/edit_cmd.py rename to pubs/commands/edit_cmd.py index 2f8dcd3..676e545 100644 --- a/papers/commands/edit_cmd.py +++ b/pubs/commands/edit_cmd.py @@ -1,7 +1,6 @@ -from ..files import editor_input +from ..content import editor_input from .. import repo from ..paper import get_bibentry_from_string, get_safe_metadata_from_content -from .helpers import add_references_argument, parse_reference from ..configs import config from ..uis import get_ui @@ -11,7 +10,8 @@ def parser(subparsers): help='open the paper bibliographic file in an editor') parser.add_argument('-m', '--meta', action='store_true', default=False, help='edit metadata') - add_references_argument(parser, single=True) + parser.add_argument('citekey', + help='citekey of the paper') return parser @@ -19,12 +19,15 @@ def command(args): ui = get_ui() meta = args.meta - reference = args.reference + citekey = args.citekey rp = repo.Repository(config()) - key = parse_reference(rp, reference) - paper = rp.get_paper(key) - filepath = rp._metafile(key) if meta else rp._bibfile(key) + coder = endecoder.EnDecoder() + if meta: + filepath = os.path.join(rp.databroker.databroker.filebroker.metadir(), citekey+'.yaml') + else: + filepath = os.path.join(rp.databroker.databroker.filebroker.bibdir(), citekey+'.bibyaml') + with open(filepath) as f: content = f.read() @@ -49,7 +52,7 @@ def command(args): options = ['overwrite', 'edit again', 'abort'] choice = options[ui.input_choice( options, ['o', 'e', 'a'], - question='A paper already exist with this citekey.' + question='A paper already exists with this citekey.' )] if choice == 'abort': diff --git a/papers/commands/export_cmd.py b/pubs/commands/export_cmd.py similarity index 100% rename from papers/commands/export_cmd.py rename to pubs/commands/export_cmd.py diff --git a/papers/commands/import_cmd.py b/pubs/commands/import_cmd.py similarity index 100% rename from papers/commands/import_cmd.py rename to pubs/commands/import_cmd.py diff --git a/papers/commands/init_cmd.py b/pubs/commands/init_cmd.py similarity index 79% rename from papers/commands/init_cmd.py rename to pubs/commands/init_cmd.py index 622581d..27a018d 100644 --- a/papers/commands/init_cmd.py +++ b/pubs/commands/init_cmd.py @@ -10,9 +10,9 @@ from .. import color def parser(subparsers): parser = subparsers.add_parser('init', - help="initialize the papers directory") + help="initialize the pubs directory") parser.add_argument('-p', '--pubsdir', default=None, - help='path to papers directory (if none, ~/.papers is used)') + help='path to pubs directory (if none, ~/.ubs is used)') parser.add_argument('-d', '--docsdir', default='docsdir://', help=('path to document directory (if not specified, documents will' 'be stored in /path/to/pubsdir/doc/)')) @@ -20,14 +20,14 @@ def parser(subparsers): def command(args): - """Create a .papers directory""" + """Create a .pubs directory""" ui = get_ui() pubsdir = args.pubsdir docsdir = args.docsdir if pubsdir is None: - pubsdir = '~/.papers' + pubsdir = '~/.pubs' pubsdir = os.path.normpath(os.path.abspath(os.path.expanduser(pubsdir))) @@ -36,7 +36,7 @@ def command(args): color.dye(pubsdir, color.filepath))) ui.exit() - ui.print_('Initializing papers in {}.'.format( + ui.print_('Initializing pubs in {}.'.format( color.dye(pubsdir, color.filepath))) config().pubsdir = pubsdir diff --git a/papers/commands/list_cmd.py b/pubs/commands/list_cmd.py similarity index 100% rename from papers/commands/list_cmd.py rename to pubs/commands/list_cmd.py diff --git a/papers/commands/note_cmd.py b/pubs/commands/note_cmd.py similarity index 100% rename from papers/commands/note_cmd.py rename to pubs/commands/note_cmd.py diff --git a/papers/commands/open_cmd.py b/pubs/commands/open_cmd.py similarity index 100% rename from papers/commands/open_cmd.py rename to pubs/commands/open_cmd.py diff --git a/papers/commands/remove_cmd.py b/pubs/commands/remove_cmd.py similarity index 100% rename from papers/commands/remove_cmd.py rename to pubs/commands/remove_cmd.py diff --git a/papers/commands/rename_cmd.py b/pubs/commands/rename_cmd.py similarity index 100% rename from papers/commands/rename_cmd.py rename to pubs/commands/rename_cmd.py diff --git a/papers/commands/tag_cmd.py b/pubs/commands/tag_cmd.py similarity index 94% rename from papers/commands/tag_cmd.py rename to pubs/commands/tag_cmd.py index 2395a9d..d978eb1 100644 --- a/papers/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -1,19 +1,19 @@ """ This command is all about tags. The different use cases are : -1. > papers tag +1. > pubs tag Returns the list of all tags -2. > papers tag citekey +2. > pubs tag citekey Return the list of tags of the given citekey -3. > papers tag citekey math +3. > pubs tag citekey math Add 'math' to the list of tags of the given citekey -4. > papers tag citekey :math +4. > pubs tag citekey :math Remove 'math' for the list of tags of the given citekey -5. > papers tag citekey math+romance-war +5. > pubs tag citekey math+romance-war Add 'math' and 'romance' tags to the given citekey, and remove the 'war' tag -6. > papers tag math +6. > pubs tag math If 'math' is not a citekey, then display all papers with the tag 'math' -7. > papers tag -war+math+romance +7. > pubs tag -war+math+romance display all papers with the tag 'math', 'romance' but not 'war' """ diff --git a/pubs/commands/update_cmd.py b/pubs/commands/update_cmd.py new file mode 100644 index 0000000..48b1741 --- /dev/null +++ b/pubs/commands/update_cmd.py @@ -0,0 +1,37 @@ +import sys + +from .. import repo +from .. import color +from ..configs import config +from ..uis import get_ui +from ..__init__ import __version__ + +def parser(subparsers): + parser = subparsers.add_parser('update', help='update the repository to the lastest format') + return parser + + +def command(args): + + ui = get_ui() + + code_version = __version__ + repo_version = int(config().version) + + if repo_version == code_version: + ui.print_('Your pubs repository is up-to-date.') + sys.exit(0) + elif repo_version > code_version: + ui.print_('Your repository was generated with an newer version of pubs.\n' + 'You should not use pubs until you install the newest version.') + sys.exit(0) + else: + msg = ("You should backup the pubs directory {} before continuing." + "Continue ?").format(color.dye(config().papers_dir, color.filepath)) + sure = ui.input_yn(question=msg, default='n') + if not sure: + sys.exit(0) + + +# config().version = repo_version +# config().save() diff --git a/papers/commands/websearch_cmd.py b/pubs/commands/websearch_cmd.py similarity index 100% rename from papers/commands/websearch_cmd.py rename to pubs/commands/websearch_cmd.py diff --git a/papers/configs.py b/pubs/configs.py similarity index 94% rename from papers/configs.py rename to pubs/configs.py index c538154..9d1a446 100644 --- a/papers/configs.py +++ b/pubs/configs.py @@ -5,8 +5,8 @@ from .p3 import configparser # constant stuff (DFT = DEFAULT) -MAIN_SECTION = 'papers' -DFT_CONFIG_PATH = os.path.expanduser('~/.papersrc') +MAIN_SECTION = 'pubs' +DFT_CONFIG_PATH = os.path.expanduser('~/.pubsrc') try: DFT_EDIT_CMD = os.environ['EDITOR'] except KeyError: @@ -15,7 +15,7 @@ except KeyError: DFT_PLUGINS = '' DFT_CONFIG = collections.OrderedDict([ - ('pubsdir', os.path.expanduser('~/.papers')), + ('pubsdir', os.path.expanduser('~/.pubs')), ('docsdir', ''), ('import_copy', True), ('import_move', False), diff --git a/papers/content.py b/pubs/content.py similarity index 100% rename from papers/content.py rename to pubs/content.py diff --git a/papers/databroker.py b/pubs/databroker.py similarity index 100% rename from papers/databroker.py rename to pubs/databroker.py diff --git a/papers/datacache.py b/pubs/datacache.py similarity index 100% rename from papers/datacache.py rename to pubs/datacache.py diff --git a/papers/endecoder.py b/pubs/endecoder.py similarity index 100% rename from papers/endecoder.py rename to pubs/endecoder.py diff --git a/papers/events.py b/pubs/events.py similarity index 100% rename from papers/events.py rename to pubs/events.py diff --git a/papers/filebroker.py b/pubs/filebroker.py similarity index 100% rename from papers/filebroker.py rename to pubs/filebroker.py diff --git a/papers/p3.py b/pubs/p3.py similarity index 100% rename from papers/p3.py rename to pubs/p3.py diff --git a/papers/paper.py b/pubs/paper.py similarity index 100% rename from papers/paper.py rename to pubs/paper.py diff --git a/papers/plugins.py b/pubs/plugins.py similarity index 95% rename from papers/plugins.py rename to pubs/plugins.py index af33e4a..d7c54cd 100644 --- a/papers/plugins.py +++ b/pubs/plugins.py @@ -35,7 +35,7 @@ def load_plugins(ui, names): PapersPlugin subclasses desired. """ for name in names: - modname = '%s.%s.%s.%s' % ('papers', PLUGIN_NAMESPACE, name, name) + modname = '%s.%s.%s.%s' % ('pubs', PLUGIN_NAMESPACE, name, name) try: namespace = importlib.import_module(modname) except ImportError as exc: diff --git a/papers/plugs/__init__.py b/pubs/plugs/__init__.py similarity index 100% rename from papers/plugs/__init__.py rename to pubs/plugs/__init__.py diff --git a/papers/plugs/alias/__init__.py b/pubs/plugs/alias/__init__.py similarity index 100% rename from papers/plugs/alias/__init__.py rename to pubs/plugs/alias/__init__.py diff --git a/papers/plugs/alias/alias.py b/pubs/plugs/alias/alias.py similarity index 98% rename from papers/plugs/alias/alias.py rename to pubs/plugs/alias/alias.py index 078f8ac..0c6da43 100644 --- a/papers/plugs/alias/alias.py +++ b/pubs/plugs/alias/alias.py @@ -3,7 +3,7 @@ import shlex from ...plugins import PapersPlugin from ...configs import config -from ...papers_cmd import execute +from ...pubs_cmd import execute class Alias(object): diff --git a/papers/plugs/texnote/__init__.py b/pubs/plugs/texnote/__init__.py similarity index 100% rename from papers/plugs/texnote/__init__.py rename to pubs/plugs/texnote/__init__.py diff --git a/papers/plugs/texnote/autofill_tools.py b/pubs/plugs/texnote/autofill_tools.py similarity index 100% rename from papers/plugs/texnote/autofill_tools.py rename to pubs/plugs/texnote/autofill_tools.py diff --git a/papers/plugs/texnote/default_body.tex b/pubs/plugs/texnote/default_body.tex similarity index 100% rename from papers/plugs/texnote/default_body.tex rename to pubs/plugs/texnote/default_body.tex diff --git a/papers/plugs/texnote/default_style.sty b/pubs/plugs/texnote/default_style.sty similarity index 100% rename from papers/plugs/texnote/default_style.sty rename to pubs/plugs/texnote/default_style.sty diff --git a/papers/plugs/texnote/latex_tools.py b/pubs/plugs/texnote/latex_tools.py similarity index 100% rename from papers/plugs/texnote/latex_tools.py rename to pubs/plugs/texnote/latex_tools.py diff --git a/papers/plugs/texnote/texnote.py b/pubs/plugs/texnote/texnote.py similarity index 100% rename from papers/plugs/texnote/texnote.py rename to pubs/plugs/texnote/texnote.py diff --git a/papers/pretty.py b/pubs/pretty.py similarity index 100% rename from papers/pretty.py rename to pubs/pretty.py diff --git a/pubs/pubs b/pubs/pubs new file mode 100755 index 0000000..99991d4 --- /dev/null +++ b/pubs/pubs @@ -0,0 +1,5 @@ +#!/usr/bin/env python2 +# -*- coding:utf-8 -*- + +from pubs import pubs_cmd +pubs_cmd.execute() \ No newline at end of file diff --git a/papers/papers_cmd.py b/pubs/pubs_cmd.py similarity index 92% rename from papers/papers_cmd.py rename to pubs/pubs_cmd.py index 0433fc5..7f513e7 100644 --- a/papers/papers_cmd.py +++ b/pubs/pubs_cmd.py @@ -41,17 +41,17 @@ def _update_check(config, ui): if repo_version > code_version: ui.warning( 'your repository was generated with an newer version' - ' of papers (v{}) than the one you are using (v{}).' + ' of pubs (v{}) than the one you are using (v{}).' '\n'.format(repo_version, code_version) + - 'You should not use papers until you install the ' - 'newest version. (use version_warning in you papersrc ' + 'You should not use pubs until you install the ' + 'newest version. (use version_warning in you pubsrc ' 'to bypass this error)') sys.exit() elif repo_version < code_version: ui.print_( 'warning: your repository version (v{})'.format(repo_version) + 'must be updated to version {}.\n'.format(code_version) - + "run 'papers update'.") + + "run 'pubs update'.") sys.exit() diff --git a/papers/repo.py b/pubs/repo.py similarity index 100% rename from papers/repo.py rename to pubs/repo.py diff --git a/papers/uis.py b/pubs/uis.py similarity index 100% rename from papers/uis.py rename to pubs/uis.py diff --git a/setup.py b/setup.py index 05fd26f..3a9a9fe 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -setup(name='papers', +setup(name='pubs', version='4', author='Fabien Benureau, Olivier Mangin, Jonathan Grizou', author_email='fabien.benureau+inria@gmail.com', @@ -11,7 +11,7 @@ setup(name='papers', requires=['pybtex'], packages=find_packages(), package_data={'': ['*.tex', '*.sty']}, - scripts=['papers/papers'] + scripts=['pubs/pubs'] ) # TODO include proper package data from plugins (08/06/2013) diff --git a/tests/fake_env.py b/tests/fake_env.py index a8f480c..09c49e3 100644 --- a/tests/fake_env.py +++ b/tests/fake_env.py @@ -11,8 +11,8 @@ import fake_filesystem import fake_filesystem_shutil import fake_filesystem_glob -from papers import color -from papers.p3 import io, input +from pubs import color +from pubs.p3 import io, input # code for fake fs @@ -26,13 +26,13 @@ real_glob = glob # def _mod_list(): # ml = [] -# import papers +# import pubs # for importer, modname, ispkg in pkgutil.walk_packages( -# path=papers.__path__, -# prefix=papers.__name__ + '.', +# path=pubs.__path__, +# prefix=pubs.__name__ + '.', # onerror=lambda x: None): # # HACK to not load textnote -# if not modname.startswith('papers.plugs.texnote'): +# if not modname.startswith('pubs.plugs.texnote'): # ml.append((modname, __import__(modname, fromlist='dummy'))) # return ml diff --git a/tests/fixtures.py b/tests/fixtures.py index 23f1413..78b15e0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,8 @@ from pybtex.database import Person -from papers.paper import Paper, get_bibentry_from_string +import testenv +from pubs import endecoder +import str_fixtures turing1950 = Paper() turing1950.bibentry.fields['title'] = u'Computing machinery and intelligence.' @@ -16,35 +18,7 @@ doe2013.bibentry.fields['year'] = u'2013' doe2013.bibentry.persons['author'] = [Person(u'John Doe')] doe2013.citekey = doe2013.generate_citekey() +coder = endecoder.EnDecoder() +bibdata = coder.decode_bibdata(str_fixtures.bibtex_external0, fmt='bibtex') +page99 = Paper(bibdata) -pagerankbib = """ -@techreport{Page99, - number = {1999-66}, - month = {November}, - author = {Lawrence Page and Sergey Brin and Rajeev Motwani and Terry Winograd}, - note = {Previous number = SIDL-WP-1999-0120}, - title = {The PageRank Citation Ranking: Bringing Order to the Web.}, - type = {Technical Report}, - publisher = {Stanford InfoLab}, - year = {1999}, -institution = {Stanford InfoLab}, - url = {http://ilpubs.stanford.edu:8090/422/}, -} -""" - -page99 = Paper(bibentry=get_bibentry_from_string(pagerankbib)[1]) - -pagerankbib_generated = """@techreport{ - Page99, - author = "Page, Lawrence and Brin, Sergey and Motwani, Rajeev and Winograd, Terry", - publisher = "Stanford InfoLab", - title = "The PageRank Citation Ranking: Bringing Order to the Web.", - url = "http://ilpubs.stanford.edu:8090/422/", - number = "1999-66", - month = "November", - note = "Previous number = SIDL-WP-1999-0120", - year = "1999", - institution = "Stanford InfoLab" -} - -""" diff --git a/tests/test.sh b/tests/test.sh index c9e2521..fcb7404 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash rm -Rf tmpdir/*; -papers init -p tmpdir/; -papers add -d data/pagerank.pdf -b data/pagerank.bib; -papers list; -papers tag; -papers tag Page99 network+search; -papers tag Page99; -papers tag search; -papers tag 0; +pubs init -p tmpdir/; +pubs add -d data/pagerank.pdf -b data/pagerank.bib; +pubs list; +pubs tag; +pubs tag Page99 network+search; +pubs tag Page99; +pubs tag search; +pubs tag 0; #rm -Rf tmpdir/*; diff --git a/tests/test_color.py b/tests/test_color.py index 4dd1b01..6c08b28 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -1,5 +1,5 @@ import testenv -from papers import color +from pubs import color def perf_color(): s = str(range(1000)) diff --git a/tests/test_config.py b/tests/test_config.py index 1f7faa6..d1fe85f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,9 +2,9 @@ import unittest import testenv -from papers import configs -from papers.configs import config -from papers.p3 import configparser +from pubs import configs +from pubs.configs import config +from pubs.p3 import configparser class TestConfig(unittest.TestCase): @@ -17,7 +17,7 @@ class TestConfig(unittest.TestCase): a = configs.Config() a.as_global() - self.assertEqual(config().papers_dir, configs.DFT_CONFIG['papers_dir']) + self.assertEqual(config().pubs_dir, configs.DFT_CONFIG['pubs_dir']) self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color'])) def test_set(self): @@ -25,11 +25,11 @@ class TestConfig(unittest.TestCase): a.as_global() config().color = 'no' self.assertEqual(config().color, False) - self.assertEqual(config('papers').color, False) + self.assertEqual(config('pubs').color, False) # booleans type for new variables are memorized, but not saved. config().bla = True self.assertEqual(config().bla, True) - self.assertEqual(config('papers').bla, True) + self.assertEqual(config('pubs').bla, True) with self.assertRaises(configparser.NoOptionError): config()._cfg.get(configs.MAIN_SECTION, '_section') @@ -65,5 +65,5 @@ class TestConfig(unittest.TestCase): self.assertEqual(config(section = 'bla3').get('color', default = config().color), True) def test_keywords(self): - a = configs.Config(papers_dir = '/blabla') - self.assertEqual(a.papers_dir, '/blabla') + a = configs.Config(pubs_dir = '/blabla') + self.assertEqual(a.pubs_dir, '/blabla') diff --git a/tests/test_databroker.py b/tests/test_databroker.py index 7e4971a..daab329 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -5,10 +5,10 @@ import os import testenv import fake_env -from papers import content, filebroker, databroker, datacache +from pubs import content, filebroker, databroker, datacache import str_fixtures -from papers import endecoder +from pubs import endecoder class TestFakeFs(unittest.TestCase): """Abstract TestCase intializing the fake filesystem.""" diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py index 2e31e76..4c6910f 100644 --- a/tests/test_endecoder.py +++ b/tests/test_endecoder.py @@ -4,7 +4,7 @@ import unittest import yaml import testenv -from papers import endecoder +from pubs import endecoder from str_fixtures import bibyaml_raw0, bibtexml_raw0, bibtex_raw0, metadata_raw0 diff --git a/tests/test_events.py b/tests/test_events.py index 0339d84..c12ec7f 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,7 +1,7 @@ from unittest import TestCase import testenv -from papers.events import Event +from pubs.events import Event _output = None diff --git a/tests/test_filebroker.py b/tests/test_filebroker.py index 32833a8..47c257d 100644 --- a/tests/test_filebroker.py +++ b/tests/test_filebroker.py @@ -5,7 +5,7 @@ import os import testenv import fake_env -from papers import content, filebroker +from pubs import content, filebroker class TestFakeFs(unittest.TestCase): """Abstract TestCase intializing the fake filesystem.""" diff --git a/tests/test_queries.py b/tests/test_queries.py index 0b63af2..bafa009 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -66,24 +66,24 @@ class TestCheckQueryBlock(TestCase): class TestFilterPaper(TestCase): def test_case(self): - self.assertTrue(filter_paper(fixtures.doe2013, ['title:nice'])) - self.assertTrue(filter_paper(fixtures.doe2013, ['title:Nice'])) + self.assertTrue (filter_paper(fixtures.doe2013, ['title:nice'])) + self.assertTrue (filter_paper(fixtures.doe2013, ['title:Nice'])) self.assertFalse(filter_paper(fixtures.doe2013, ['title:nIce'])) def test_fields(self): - self.assertTrue(filter_paper(fixtures.doe2013, ['year:2013'])) + self.assertTrue (filter_paper(fixtures.doe2013, ['year:2013'])) self.assertFalse(filter_paper(fixtures.doe2013, ['year:2014'])) - self.assertTrue(filter_paper(fixtures.doe2013, ['author:doe'])) - self.assertTrue(filter_paper(fixtures.doe2013, ['author:Doe'])) + self.assertTrue (filter_paper(fixtures.doe2013, ['author:doe'])) + self.assertTrue (filter_paper(fixtures.doe2013, ['author:Doe'])) def test_tags(self): - self.assertTrue(filter_paper(fixtures.turing1950, ['tag:computer'])) + self.assertTrue (filter_paper(fixtures.turing1950, ['tag:computer'])) self.assertFalse(filter_paper(fixtures.turing1950, ['tag:Ai'])) - self.assertTrue(filter_paper(fixtures.turing1950, ['tag:AI'])) - self.assertTrue(filter_paper(fixtures.turing1950, ['tag:ai'])) + self.assertTrue (filter_paper(fixtures.turing1950, ['tag:AI'])) + self.assertTrue (filter_paper(fixtures.turing1950, ['tag:ai'])) def test_multiple(self): - self.assertTrue(filter_paper(fixtures.doe2013, + self.assertTrue (filter_paper(fixtures.doe2013, ['author:doe', 'year:2013'])) self.assertFalse(filter_paper(fixtures.doe2013, ['author:doe', 'year:2014'])) diff --git a/tests/test_repo.py b/tests/test_repo.py index 398a742..b57372c 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -4,10 +4,10 @@ import shutil import os import fixtures -from papers.repo import (Repository, _base27, BIB_DIR, META_DIR, +from pubs.repo import (Repository, _base27, BIB_DIR, META_DIR, CiteKeyCollision) -from papers.paper import PaperInRepo -from papers import configs, files +from pubs.paper import PaperInRepo +from pubs import configs, files class TestCitekeyGeneration(unittest.TestCase): @@ -30,7 +30,7 @@ class TestRepo(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() - self.repo = Repository(configs.Config(papers_dir=self.tmpdir), load=False) + self.repo = Repository(configs.Config(pubs_dir=self.tmpdir), load=False) self.repo.save() self.repo.add_paper(fixtures.turing1950) diff --git a/tests/test_tag.py b/tests/test_tag.py index 299bdf9..f2db3a7 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -2,7 +2,7 @@ import unittest import testenv -from papers.commands.tag_cmd import _parse_tags, _tag_groups +from pubs.commands.tag_cmd import _parse_tags, _tag_groups class TestTag(unittest.TestCase): diff --git a/tests/test_usecase.py b/tests/test_usecase.py index b6c7c77..903a0f1 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -5,12 +5,12 @@ import os import testenv import fake_env -from papers import papers_cmd -from papers import color, content, filebroker, uis, beets_ui, p3 +from pubs import pubs_cmd +from pubs import color, content, filebroker, uis, beets_ui, p3 import str_fixtures -from papers.commands import init_cmd, import_cmd +from pubs.commands import init_cmd, import_cmd # code for fake fs @@ -65,7 +65,7 @@ class CommandTestCase(unittest.TestCase): input = fake_env.FakeInput(cmd[1], [content, uis, beets_ui, p3]) input.as_global() - _, stdout, stderr = fake_env.redirect(papers_cmd.execute)(cmd[0].split()) + _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd[0].split()) if len(cmd) == 3: actual_out = color.undye(stdout.getvalue()) correct_out = color.undye(cmd[2]) @@ -73,7 +73,7 @@ class CommandTestCase(unittest.TestCase): else: assert type(cmd) == str - _, stdout, stderr = fake_env.redirect(papers_cmd.execute)(cmd.split()) + _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd.split()) assert(stderr.getvalue() == '') outs.append(color.undye(stdout.getvalue())) @@ -97,28 +97,28 @@ class DataCommandTestCase(CommandTestCase): class TestInit(CommandTestCase): def test_init(self): - pubsdir = os.path.expanduser('~/papers_test2') - papers_cmd.execute('papers init -p {}'.format(pubsdir).split()) + pubsdir = os.path.expanduser('~/pubs_test2') + pubs_cmd.execute('pubs init -p {}'.format(pubsdir).split()) self.assertEqual(set(self.fs['os'].listdir(pubsdir)), {'bib', 'doc', 'meta', 'notes'}) def test_init2(self): - pubsdir = os.path.expanduser('~/.papers') - papers_cmd.execute('papers init'.split()) + pubsdir = os.path.expanduser('~/.pubs') + pubs_cmd.execute('pubs init'.split()) self.assertEqual(set(self.fs['os'].listdir(pubsdir)), {'bib', 'doc', 'meta', 'notes'}) class TestAdd(DataCommandTestCase): def test_add(self): - cmds = ['papers init', - 'papers add -b /data/pagerank.bib -d /data/pagerank.pdf', + cmds = ['pubs init', + 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', ] self.execute_cmds(cmds) def test_add2(self): - cmds = ['papers init -p /not_default', - 'papers add -b /data/pagerank.bib -d /data/pagerank.pdf', + cmds = ['pubs init -p /not_default', + 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', ] self.execute_cmds(cmds) self.assertEqual(set(self.fs['os'].listdir('/not_default/doc')), {'Page99.pdf'}) @@ -127,37 +127,37 @@ class TestAdd(DataCommandTestCase): class TestList(DataCommandTestCase): def test_list(self): - cmds = ['papers init -p /not_default2', - 'papers list', - 'papers add -b /data/pagerank.bib -d /data/pagerank.pdf', - 'papers list', + cmds = ['pubs init -p /not_default2', + 'pubs list', + 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', + 'pubs list', ] self.execute_cmds(cmds) def test_list_smart_case(self): - cmds = ['papers init', - 'papers list', - 'papers import data/', - 'papers list title:language author:Saunders', + cmds = ['pubs init', + 'pubs list', + 'pubs import data/', + 'pubs list title:language author:Saunders', ] outs = self.execute_cmds(cmds) print outs[-1] self.assertEquals(1, len(outs[-1].split('/n'))) def test_list_ignore_case(self): - cmds = ['papers init', - 'papers list', - 'papers import data/', - 'papers list --ignore-case title:lAnguAge author:saunders', + cmds = ['pubs init', + 'pubs list', + 'pubs import data/', + 'pubs list --ignore-case title:lAnguAge author:saunders', ] outs = self.execute_cmds(cmds) self.assertEquals(1, len(outs[-1].split('/n'))) def test_list_force_case(self): - cmds = ['papers init', - 'papers list', - 'papers import data/', - 'papers list --force-case title:Language author:saunders', + cmds = ['pubs init', + 'pubs list', + 'pubs import data/', + 'pubs list --force-case title:Language author:saunders', ] outs = self.execute_cmds(cmds) self.assertEquals(0 + 1, len(outs[-1].split('/n'))) @@ -167,7 +167,7 @@ class TestList(DataCommandTestCase): class TestUsecase(DataCommandTestCase): def test_first(self): - correct = ['Initializing papers in /paper_first.\n', + correct = ['Initializing pubs in /paper_first.\n', '', '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n', '', @@ -176,55 +176,55 @@ class TestUsecase(DataCommandTestCase): '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) search network\n' ] - cmds = ['papers init -p paper_first/', - 'papers add -d data/pagerank.pdf -b data/pagerank.bib', - 'papers list', - 'papers tag', - 'papers tag Page99 network+search', - 'papers tag Page99', - 'papers tag search', + cmds = ['pubs init -p paper_first/', + 'pubs add -d data/pagerank.pdf -b data/pagerank.bib', + 'pubs list', + 'pubs tag', + 'pubs tag Page99 network+search', + 'pubs tag Page99', + 'pubs tag search', ] self.assertEqual(correct, self.execute_cmds(cmds)) def test_second(self): - cmds = ['papers init -p paper_second/', - 'papers add -b data/pagerank.bib', - 'papers add -d data/turing-mind-1950.pdf -b data/turing1950.bib', - 'papers add -b data/martius.bib', - 'papers add -b data/10.1371%2Fjournal.pone.0038236.bib', - 'papers list', - 'papers attach Page99 data/pagerank.pdf' + cmds = ['pubs init -p paper_second/', + 'pubs add -b data/pagerank.bib', + 'pubs add -d data/turing-mind-1950.pdf -b data/turing1950.bib', + 'pubs add -b data/martius.bib', + 'pubs add -b data/10.1371%2Fjournal.pone.0038236.bib', + 'pubs list', + 'pubs attach Page99 data/pagerank.pdf' ] self.execute_cmds(cmds) def test_third(self): - cmds = ['papers init', - 'papers add -b data/pagerank.bib', - 'papers add -d data/turing-mind-1950.pdf -b data/turing1950.bib', - 'papers add -b data/martius.bib', - 'papers add -b data/10.1371%2Fjournal.pone.0038236.bib', - 'papers list', - 'papers attach Page99 data/pagerank.pdf', - ('papers remove Page99', ['y']), - 'papers remove -f turing1950computing', + cmds = ['pubs init', + 'pubs add -b data/pagerank.bib', + 'pubs add -d data/turing-mind-1950.pdf -b data/turing1950.bib', + 'pubs add -b data/martius.bib', + 'pubs add -b data/10.1371%2Fjournal.pone.0038236.bib', + 'pubs list', + 'pubs attach Page99 data/pagerank.pdf', + ('pubs remove Page99', ['y']), + 'pubs remove -f turing1950computing', ] self.execute_cmds(cmds) def test_editor_abort(self): with self.assertRaises(SystemExit): - cmds = ['papers init', - ('papers add', ['abc', 'n']), - ('papers add', ['abc', 'y', 'abc', 'n']), - 'papers add -b data/pagerank.bib', - ('papers edit Page99', ['', 'a']), + cmds = ['pubs init', + ('pubs add', ['abc', 'n']), + ('pubs add', ['abc', 'y', 'abc', 'n']), + 'pubs add -b data/pagerank.bib', + ('pubs edit Page99', ['', 'a']), ] self.execute_cmds(cmds) def test_editor_success(self): - cmds = ['papers init', - ('papers add', [str_fixtures.bibtex_external0]), - ('papers remove Page99', ['y']), + cmds = ['pubs init', + ('pubs add', [str_fixtures.bibtex_external0]), + ('pubs remove Page99', ['y']), ] self.execute_cmds(cmds) @@ -239,64 +239,64 @@ class TestUsecase(DataCommandTestCase): line2 = re.sub('L. Page', 'L. Ridge', line1) line3 = re.sub('Page99', 'Ridge07', line2) - cmds = ['papers init', - 'papers add -b data/pagerank.bib', - ('papers list', [], line), - ('papers edit Page99', [bib1]), - ('papers list', [], line1), - ('papers edit Page99', [bib2]), - ('papers list', [], line2), - ('papers edit Page99', [bib3]), - ('papers list', [], line3), + cmds = ['pubs init', + 'pubs add -b data/pagerank.bib', + ('pubs list', [], line), + ('pubs edit Page99', [bib1]), + ('pubs list', [], line1), + ('pubs edit Page99', [bib2]), + ('pubs list', [], line2), + ('pubs edit Page99', [bib3]), + ('pubs list', [], line3), ] self.execute_cmds(cmds) def test_export(self): - cmds = ['papers init', - ('papers add', [str_fixtures.bibtex_external0]), - 'papers export Page99', - ('papers export Page99 -f bibtex', [], str_fixtures.bibtex_raw0), - 'papers export Page99 -f bibyaml', + cmds = ['pubs init', + ('pubs add', [str_fixtures.bibtex_external0]), + 'pubs export Page99', + ('pubs export Page99 -f bibtex', [], str_fixtures.bibtex_raw0), + 'pubs export Page99 -f bibyaml', ] self.execute_cmds(cmds) def test_import(self): - cmds = ['papers init', - 'papers import data/', - 'papers list' + cmds = ['pubs init', + 'pubs import data/', + 'pubs list' ] outs = self.execute_cmds(cmds) self.assertEqual(4 + 1, len(outs[-1].split('\n'))) def test_import_one(self): - cmds = ['papers init', - 'papers import data/ Page99', - 'papers list' + cmds = ['pubs init', + 'pubs import data/ Page99', + 'pubs list' ] outs = self.execute_cmds(cmds) self.assertEqual(1 + 1, len(outs[-1].split('\n'))) def test_open(self): - cmds = ['papers init', - 'papers add -b data/pagerank.bib', - 'papers open Page99' + cmds = ['pubs init', + 'pubs add -b data/pagerank.bib', + 'pubs open Page99' ] with self.assertRaises(SystemExit): self.execute_cmds(cmds) with self.assertRaises(SystemExit): - cmds[-1] == 'papers open Page8' + cmds[-1] == 'pubs open Page8' self.execute_cmds(cmds) def test_update(self): - cmds = ['papers init', - 'papers add -b data/pagerank.bib', - 'papers update' + cmds = ['pubs init', + 'pubs add -b data/pagerank.bib', + 'pubs update' ] with self.assertRaises(SystemExit): From 2078876168bdc212f90aa64ec569eaf031aac5f9 Mon Sep 17 00:00:00 2001 From: humm Date: Thu, 14 Nov 2013 18:37:27 +0100 Subject: [PATCH 31/48] default bibfile formant is bibtex + fixed a bug in get_content --- pubs/content.py | 3 ++- pubs/endecoder.py | 10 +++++----- pubs/filebroker.py | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pubs/content.py b/pubs/content.py index 113614b..da42d2f 100644 --- a/pubs/content.py +++ b/pubs/content.py @@ -69,7 +69,8 @@ def check_content(path): def get_content(path): """Will be useful when we need to get content from url""" if content_type(path) == 'url': - return urllib2.urlopen(path) + response = urllib2.urlopen(path) + return response.read() else: return read_file(path) diff --git a/pubs/endecoder.py b/pubs/endecoder.py index aeac06b..894784f 100644 --- a/pubs/endecoder.py +++ b/pubs/endecoder.py @@ -32,13 +32,13 @@ class EnDecoder(object): * encode_bibdata will try to recognize exceptions """ - decode_fmt = {'bibyaml' : pybtex.database.input.bibyaml, - 'bibtex' : pybtex.database.input.bibtex, + decode_fmt = {'bibtex' : pybtex.database.input.bibtex, + 'bibyaml' : pybtex.database.input.bibyaml, 'bib' : pybtex.database.input.bibtex, 'bibtexml': pybtex.database.input.bibtexml} - encode_fmt = {'bibyaml' : pybtex.database.output.bibyaml, - 'bibtex' : pybtex.database.output.bibtex, + encode_fmt = {'bibtex' : pybtex.database.output.bibtex, + 'bibyaml' : pybtex.database.output.bibyaml, 'bib' : pybtex.database.output.bibtex, 'bibtexml': pybtex.database.output.bibtexml} @@ -48,7 +48,7 @@ class EnDecoder(object): def decode_metadata(self, metadata_raw): return yaml.safe_load(metadata_raw) - def encode_bibdata(self, bibdata, fmt='bibyaml'): + def encode_bibdata(self, bibdata, fmt='bib'): """Encode bibdata """ s = StringIO.StringIO() EnDecoder.encode_fmt[fmt].Writer().write_stream(bibdata, s) diff --git a/pubs/filebroker.py b/pubs/filebroker.py index 665e1cf..37b675d 100644 --- a/pubs/filebroker.py +++ b/pubs/filebroker.py @@ -44,7 +44,7 @@ class FileBroker(object): return read_file(filepath) def pull_bibfile(self, citekey): - filepath = os.path.join(self.bibdir, citekey + '.bibyaml') + filepath = os.path.join(self.bibdir, citekey + '.bib') return read_file(filepath) def push_metafile(self, citekey, metadata): @@ -54,7 +54,7 @@ class FileBroker(object): def push_bibfile(self, citekey, bibdata): """Put content to disk. Will gladly override anything standing in its way.""" - filepath = os.path.join(self.bibdir, citekey + '.bibyaml') + filepath = os.path.join(self.bibdir, citekey + '.bib') write_file(filepath, bibdata) def push(self, citekey, metadata, bibdata): @@ -66,17 +66,17 @@ class FileBroker(object): metafilepath = os.path.join(self.metadir, citekey + '.yaml') if check_file(metafilepath): os.remove(metafilepath) - bibfilepath = os.path.join(self.bibdir, citekey + '.bibyaml') + bibfilepath = os.path.join(self.bibdir, citekey + '.bib') if check_file(bibfilepath): os.remove(bibfilepath) def exists(self, citekey, both=True): if both: return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) and - check_file(os.path.join(self.bibdir, citekey + '.bibyaml'), fail=False)) + check_file(os.path.join(self.bibdir, citekey + '.bib'), fail=False)) else: return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) or - check_file(os.path.join(self.bibdir, citekey + '.bibyaml'), fail=False)) + check_file(os.path.join(self.bibdir, citekey + '.bib'), fail=False)) def listing(self, filestats=True): @@ -92,7 +92,7 @@ class FileBroker(object): bibfiles = [] for filename in os.listdir(self.bibdir): - citekey = filter_filename(filename, '.bibyaml') + citekey = filter_filename(filename, '.bib') if citekey is not None: if filestats: stats = os.stat(os.path.join(path, filename)) @@ -163,7 +163,7 @@ class DocBroker(object): doc_content = get_content(full_source_path) with open(full_target_path, 'wb') as f: - f.write(doc_content.read()) + f.write(doc_content) return target_path @@ -193,5 +193,7 @@ class DocBroker(object): if not self.in_docsdir(docpath): raise ValueError('cannot rename an external file ({}).'.format(docpath)) - new_notepath = self.add_doc(new_citekey, docpath) - self.remove_doc(docpath) \ No newline at end of file + new_docpath = self.add_doc(new_citekey, docpath) + self.remove_doc(docpath) + + return new_docpath \ No newline at end of file From c4f296346a9a297b581309cdf183bfb59609a139 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Fri, 15 Nov 2013 13:25:54 +0100 Subject: [PATCH 32/48] add template text to add editor input --- pubs/commands/add_cmd.py | 22 ++++++++++++++++------ pubs/templates/__init__.py | 1 + pubs/templates/add_bib.txt | 18 ++++++++++++++++++ pubs/templates/str_templates.py | 20 ++++++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 pubs/templates/__init__.py create mode 100644 pubs/templates/add_bib.txt create mode 100644 pubs/templates/str_templates.py diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 6b80e83..60afce5 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -4,6 +4,8 @@ from .. import bibstruct from .. import content from .. import repo from .. import paper +from .. import templates + def parser(subparsers): parser = subparsers.add_parser('add', help='add a paper to the repository') @@ -40,13 +42,21 @@ def command(args): if bibfile is None: cont = True bibstr = '' - while cont: try: - bibstr = content.editor_input(config().edit_cmd, bibstr, suffix='.yaml') - bibdata = rp.databroker.verify(bibstr) - bibstruct.verify_bibdata(bibdata) - # REFACTOR Generate citykey - cont = False + bibstr = content.editor_input(config().edit_cmd, + templates.add_bib, + suffix='.bib') + if bibstr == templates.add_bib: + cont = ui.input_yn( + question='Bibfile not edited. Edit again ?', + default='y') + if not cont: + ui.exit(0) + else: + bibdata = rp.databroker.verify(bibstr) + bibstruct.verify_bibdata(bibdata) + # REFACTOR Generate citykey + cont = False except ValueError: cont = ui.input_yn( question='Invalid bibfile. Edit again ?', diff --git a/pubs/templates/__init__.py b/pubs/templates/__init__.py new file mode 100644 index 0000000..8bb9e7b --- /dev/null +++ b/pubs/templates/__init__.py @@ -0,0 +1 @@ +from str_templates import * \ No newline at end of file diff --git a/pubs/templates/add_bib.txt b/pubs/templates/add_bib.txt new file mode 100644 index 0000000..072e065 --- /dev/null +++ b/pubs/templates/add_bib.txt @@ -0,0 +1,18 @@ +# Input the bibliographic data for your article +# Supported formats are bibtex (template below), bibyaml and bibtexml + +@article{ + YourCitekey, + author = "LastName1, FirstName1 and LastName2, FirstName2", + title = "", + journal = "", + number = "7", + pages = "23-31", + volume = "13", + year = "2013", + + doi = "", + issn = "", + keywords = "", + abstract = "" +} \ No newline at end of file diff --git a/pubs/templates/str_templates.py b/pubs/templates/str_templates.py new file mode 100644 index 0000000..624fe5c --- /dev/null +++ b/pubs/templates/str_templates.py @@ -0,0 +1,20 @@ +add_bib = """ +# Input the bibliographic data for your article +# Supported formats are bibtex (template below), bibyaml and bibtexml + +@article{ + YourCitekey, + author = "LastName1, FirstName1 and LastName2, FirstName2", + title = "", + journal = "", + number = "7", + pages = "23-31", + volume = "13", + year = "2013", + + doi = "", + issn = "", + keywords = "", + abstract = "" +} +""" \ No newline at end of file From 529e4e5950601fd5da1cfeb3df1bf7fcc1e051be Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 00:25:33 +0100 Subject: [PATCH 33/48] added timestamps --- pubs/commands/add_cmd.py | 59 +++++++++++++++++++++---------------- pubs/commands/import_cmd.py | 2 ++ pubs/paper.py | 26 ++++++++++++---- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 60afce5..0613568 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -1,3 +1,5 @@ +import datetime + from ..uis import get_ui from ..configs import config from .. import bibstruct @@ -23,6 +25,32 @@ def parser(subparsers): return parser +def bibdata_from_editor(rp): + again = True + try: + bibstr = content.editor_input(config().edit_cmd, + templates.add_bib, + suffix='.bib') + if bibstr == templates.add_bib: + cont = ui.input_yn( + question='Bibfile not edited. Edit again ?', + default='y') + if not cont: + ui.exit(0) + else: + bibdata = rp.databroker.verify(bibstr) + bibstruct.verify_bibdata(bibdata) + # REFACTOR Generate citykey + cont = False + except ValueError: + again = ui.input_yn( + question='Invalid bibfile. Edit again ?', + default='y') + if not again: + ui.exit(0) + + return bibdata + def command(args): """ :param bibfile: bibtex file (in .bib, .bibml or .yaml format. @@ -37,32 +65,10 @@ def command(args): rp = repo.Repository(config()) - # get bibfile - + # get bibfile + if bibfile is None: - cont = True - bibstr = '' - try: - bibstr = content.editor_input(config().edit_cmd, - templates.add_bib, - suffix='.bib') - if bibstr == templates.add_bib: - cont = ui.input_yn( - question='Bibfile not edited. Edit again ?', - default='y') - if not cont: - ui.exit(0) - else: - bibdata = rp.databroker.verify(bibstr) - bibstruct.verify_bibdata(bibdata) - # REFACTOR Generate citykey - cont = False - except ValueError: - cont = ui.input_yn( - question='Invalid bibfile. Edit again ?', - default='y') - if not cont: - ui.exit(0) + bibdata = bibdata_from_editor(rp) else: bibdata_raw = content.get_content(bibfile) bibdata = rp.databroker.verify(bibdata_raw) @@ -82,8 +88,9 @@ def command(args): if tags is not None: p.tags = set(tags.split(',')) - + p = paper.Paper(bibdata, citekey=citekey) + p.added = datetime.datetime.now() # document file diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 0f94ad4..4dc54d3 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -1,4 +1,5 @@ import os +import datetime from pybtex.database import Entry, BibliographyData, FieldDict, Person @@ -58,6 +59,7 @@ def many_from_path(bibpath): bibdata.entries[k] = b.entries[k] papers[k] = Paper(bibdata, citekey=k) + p.added = datetime.datetime.now() except ValueError, e: papers[k] = e return papers diff --git a/pubs/paper.py b/pubs/paper.py index feb8aa9..7f6c345 100644 --- a/pubs/paper.py +++ b/pubs/paper.py @@ -1,17 +1,21 @@ import copy import collections +import datetime from . import bibstruct -DEFAULT_META = collections.OrderedDict([('docfile', None), ('tags', set()), ('notes', [])]) -DEFAULT_META = {'docfile': None, 'tags': set(), 'notes': []} +#DEFAULT_META = collections.OrderedDict([('docfile', None), ('tags', set()), ('added', )]) +DEFAULT_META = {'docfile': None, 'tags': set(), 'added': None} class Paper(object): - """ Paper class. The object is responsible for the integrity of its data + """ Paper class. - The object is not responsible of any disk i/o. - self.bibdata is a pybtex.database.BibliographyData object + The object is not responsible of any disk I/O. + self.bibdata is a pybtex.database.BibliographyData object self.metadata is a dictionary + + The paper class provides methods to access the fields for its metadata + in a pythonic manner. """ def __init__(self, bibdata, citekey=None, metadata=None): @@ -42,6 +46,8 @@ class Paper(object): metadata=copy.deepcopy(self.metadata), bibdata=copy.deepcopy(self.bibdata)) + # docpath + @property def docpath(self): return self.metadata.get('docfile', '') @@ -69,3 +75,13 @@ class Paper(object): def remove_tag(self, tag): """Remove a tag from a paper if present.""" self.tags.discard(tag) + + # added date + + @property + def added(self): + datetime.datetime.strptime(self.metadata['added'], '%Y-%m-%d %H:%M:%S') + + @added.setter + def added(self, value): + self.metadata['added'] = value.strftime('%Y-%m-%d %H:%M:%S') From dbb17426d0ea5924e4393a52ec787f66fcefcd9e Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 17:41:22 +0100 Subject: [PATCH 34/48] add_cmd: fix bug --- pubs/commands/add_cmd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 0613568..d892684 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -11,8 +11,8 @@ from .. import templates def parser(subparsers): parser = subparsers.add_parser('add', help='add a paper to the repository') - parser.add_argument('-b', '--bibfile', - help='bibtex, bibtexml or bibyaml file', default=None) + parser.add_argument('bibfile', nargs='?', default = None, + help='bibtex, bibtexml or bibyaml file') parser.add_argument('-d', '--docfile', help='pdf or ps file', default=None) parser.add_argument('-t', '--tags', help='tags associated to the paper, separated by commas', default=None) @@ -25,7 +25,7 @@ def parser(subparsers): return parser -def bibdata_from_editor(rp): +def bibdata_from_editor(ui, rp): again = True try: bibstr = content.editor_input(config().edit_cmd, @@ -68,7 +68,7 @@ def command(args): # get bibfile if bibfile is None: - bibdata = bibdata_from_editor(rp) + bibdata = bibdata_from_editor(ui, rp) else: bibdata_raw = content.get_content(bibfile) bibdata = rp.databroker.verify(bibdata_raw) From 0b1a3514857fd3cadbeb93161cb712886758b15b Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 18:56:52 +0100 Subject: [PATCH 35/48] bibstruct tests + bug fixes --- pubs/bibstruct.py | 14 +++++------ tests/fixtures.py | 51 ++++++++++++++++++++++++++++++----------- tests/test_bibstruct.py | 29 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 tests/test_bibstruct.py diff --git a/pubs/bibstruct.py b/pubs/bibstruct.py index 6b05ce6..3c086df 100644 --- a/pubs/bibstruct.py +++ b/pubs/bibstruct.py @@ -25,9 +25,9 @@ def verify_bibdata(bibdata): raise ValueError('no entries in the bibdata.') if len(bibdata.entries) > 1: raise ValueError('ambiguous: multiple entries in the bibdata.') - + def get_entry(bibdata): - verify_bibdata(bibdata) + verify_bibdata(bibdata) return bibdata.entries.iteritems().next() def extract_citekey(bibdata): @@ -38,7 +38,7 @@ def extract_citekey(bibdata): def generate_citekey(bibdata): """ Generate a citekey from bib_data. - :param generate: if False, return the citekey defined in the file, + :param generate: if False, return the citekey defined in the file, does not generate a new one. :raise ValueError: if no author nor editor is defined. """ @@ -46,18 +46,18 @@ def generate_citekey(bibdata): author_key = 'author' if 'author' in entry.persons else 'editor' try: - first_author = self.bibentry.persons[author_key][0] + first_author = entry.persons[author_key][0] except KeyError: raise ValueError( 'No author or editor defined: cannot generate a citekey.') try: - year = self.bibentry.fields['year'] + year = entry.fields['year'] except KeyError: year = '' citekey = u'{}{}'.format(u''.join(first_author.last()), year) - + return str2citekey(citekey) - + def extract_docfile(bibdata, remove=False): """ Try extracting document file from bib data. Returns None if not found. diff --git a/tests/fixtures.py b/tests/fixtures.py index 78b15e0..b853288 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,24 +1,47 @@ +# -*- coding: utf-8 -*- + from pybtex.database import Person import testenv from pubs import endecoder import str_fixtures -turing1950 = Paper() -turing1950.bibentry.fields['title'] = u'Computing machinery and intelligence.' -turing1950.bibentry.fields['year'] = u'1950' -turing1950.bibentry.persons['author'] = [Person(u'Alan Turing')] -turing1950.citekey = turing1950.generate_citekey() -turing1950.tags = ['computer', 'AI'] +coder = endecoder.EnDecoder() + +franny_bib = """ +@article{ + Franny1961, + author = "Salinger, J. D.", + title = "Franny and Zooey", + year = "1961", +} + +""" + +doe_bib = """ +@article{ + Doe2013, + author = "Doe, John", + title = "Nice Title", + year = "2013", +} + +""" + + +franny_bibdata = coder.decode_bibdata(franny_bib) +doe_bibdata = coder.decode_bibdata(doe_bib) + + +# bibdata = coder.decode_bibdata(str_fixtures.bibtex_external0, fmt='bibtex') +# page99 = Paper(bibdata) -doe2013 = Paper() -doe2013.bibentry.fields['title'] = u'Nice title.' -doe2013.bibentry.fields['year'] = u'2013' -doe2013.bibentry.persons['author'] = [Person(u'John Doe')] -doe2013.citekey = doe2013.generate_citekey() -coder = endecoder.EnDecoder() -bibdata = coder.decode_bibdata(str_fixtures.bibtex_external0, fmt='bibtex') -page99 = Paper(bibdata) +# turing1950 = Paper() +# turing1950.bibentry.fields['title'] = u'Computing machinery and intelligence.' +# turing1950.bibentry.fields['year'] = u'1950' +# turing1950.bibentry.persons['author'] = [Person(u'Alan Turing')] +# turing1950.citekey = turing1950.generate_citekey() +# turing1950.tags = ['computer', 'AI'] diff --git a/tests/test_bibstruct.py b/tests/test_bibstruct.py new file mode 100644 index 0000000..ab16fbd --- /dev/null +++ b/tests/test_bibstruct.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import os +import unittest +import copy + +from pybtex.database import Person + +import testenv +from pubs import bibstruct + +import fixtures + + +class TestGenerateCitekey(unittest.TestCase): + + def test_escapes_chars(self): + doe_bibdata = copy.deepcopy(fixtures.doe_bibdata) + citekey, entry = bibstruct.get_entry(doe_bibdata) + entry.persons['author'] = [Person(string=u'Zôu\\@/ , John')] + key = bibstruct.generate_citekey(doe_bibdata) + + def test_simple(self): + bibdata = copy.deepcopy(fixtures.doe_bibdata) + key = bibstruct.generate_citekey(bibdata) + self.assertEqual(key, 'Doe2013') + + bibdata = copy.deepcopy(fixtures.franny_bibdata) + key = bibstruct.generate_citekey(bibdata) + self.assertEqual(key, 'Salinger1961') From 23cf48661b15ad0fcdf44e90c8993e9507862921 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 19:19:31 +0100 Subject: [PATCH 36/48] added bibstruct text, cleaned fixtures and test_paper --- tests/fixtures.py | 32 ++++++-------------------------- tests/str_fixtures.py | 19 ++++++++++++++++--- tests/test_bibstruct.py | 4 ++++ tests/test_paper.py | 15 --------------- 4 files changed, 26 insertions(+), 44 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index b853288..73bdac0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -8,40 +8,20 @@ import str_fixtures coder = endecoder.EnDecoder() -franny_bib = """ -@article{ - Franny1961, +franny_bib = """@article{Franny1961, author = "Salinger, J. D.", title = "Franny and Zooey", - year = "1961", -} - + year = "1961"} """ doe_bib = """ -@article{ - Doe2013, +@article{Doe2013, author = "Doe, John", title = "Nice Title", - year = "2013", -} - + year = "2013"} """ - franny_bibdata = coder.decode_bibdata(franny_bib) doe_bibdata = coder.decode_bibdata(doe_bib) - - -# bibdata = coder.decode_bibdata(str_fixtures.bibtex_external0, fmt='bibtex') -# page99 = Paper(bibdata) - - - - -# turing1950 = Paper() -# turing1950.bibentry.fields['title'] = u'Computing machinery and intelligence.' -# turing1950.bibentry.fields['year'] = u'1950' -# turing1950.bibentry.persons['author'] = [Person(u'Alan Turing')] -# turing1950.citekey = turing1950.generate_citekey() -# turing1950.tags = ['computer', 'AI'] +page_bibdata = coder.decode_bibdata(str_fixtures.bibtex_raw0) +turing_bibdata = coder.decode_bibdata(str_fixtures.turing_bib) diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index ba8f32a..b29bb7f 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -98,7 +98,20 @@ bibtex_raw0 = """@techreport{ """ -metadata_raw0 = """external-document: null -notes: [] +metadata_raw0 = """docfile: null tags: [search, network] -""" \ No newline at end of file +added: '2013-11-14 13:14:20' +""" + +turing_bib = """@article{turing1950computing, + title={Computing machinery and intelligence}, + author={Turing, Alan M}, + journal={Mind}, + volume={59}, + number={236}, + pages={433--460}, + year={1950}, + publisher={JSTOR} +} + +""" diff --git a/tests/test_bibstruct.py b/tests/test_bibstruct.py index ab16fbd..eb1bfb1 100644 --- a/tests/test_bibstruct.py +++ b/tests/test_bibstruct.py @@ -13,6 +13,10 @@ import fixtures class TestGenerateCitekey(unittest.TestCase): + def test_fails_on_empty_paper(self): + with self.assertRaises(ValueError): + bibstruct.generate_citekey(None) + def test_escapes_chars(self): doe_bibdata = copy.deepcopy(fixtures.doe_bibdata) citekey, entry = bibstruct.get_entry(doe_bibdata) diff --git a/tests/test_paper.py b/tests/test_paper.py index 03fa010..87cc8eb 100644 --- a/tests/test_paper.py +++ b/tests/test_paper.py @@ -36,21 +36,6 @@ class TestCreateCitekey(unittest.TestCase): with self.assertRaises(ValueError): paper.generate_citekey() - def test_escapes_chars(self): - paper = Paper() - paper.bibentry.persons['author'] = [ - Person(last=u'Z ôu\\@/', first='Zde'), - Person(string='John Doe')] - key = paper.generate_citekey() - self.assertEqual(key, 'Zou') - - def test_simple(self): - paper = Paper() - paper.bibentry.persons['author'] = [Person(string='John Doe')] - paper.bibentry.fields['year'] = '2001' - key = paper.generate_citekey() - self.assertEqual(key, 'Doe2001') - class TestSaveLoad(unittest.TestCase): From 810525b4d0cd875612b243a34ce7d5d17e49d9f8 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 19:20:42 +0100 Subject: [PATCH 37/48] fixed test_config --- tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index d1fe85f..93f6bb1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -17,7 +17,7 @@ class TestConfig(unittest.TestCase): a = configs.Config() a.as_global() - self.assertEqual(config().pubs_dir, configs.DFT_CONFIG['pubs_dir']) + self.assertEqual(config().pubsdir, configs.DFT_CONFIG['pubsdir']) self.assertEqual(config().color, configs.str2bool(configs.DFT_CONFIG['color'])) def test_set(self): From 9243859294a2c0130fe3fcf3c247474f9fe3bf0e Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 17 Nov 2013 19:44:49 +0100 Subject: [PATCH 38/48] updated test_usecase + fixed bug in import_cmd --- pubs/commands/import_cmd.py | 4 ++-- tests/str_fixtures.py | 1 + tests/test_usecase.py | 43 ++++++++++++++++++++----------------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 4dc54d3..717e586 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -59,7 +59,7 @@ def many_from_path(bibpath): bibdata.entries[k] = b.entries[k] papers[k] = Paper(bibdata, citekey=k) - p.added = datetime.datetime.now() + papers[k].added = datetime.datetime.now() except ValueError, e: papers[k] = e return papers @@ -95,7 +95,7 @@ def command(args): copy_doc = config().import_copy if copy_doc: docfile = rp.databroker.copy_doc(p.citekey, docfile) - + p.docpath = docfile rp.push_paper(p) ui.print_('{} imported'.format(color.dye(p.citekey, color.cyan))) diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index b29bb7f..eebfc97 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -93,6 +93,7 @@ bibtex_raw0 = """@techreport{ month = "November", note = "Previous number = SIDL-WP-1999-0120", year = "1999", + type = "Technical Report", institution = "Stanford InfoLab" } diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 903a0f1..5b10934 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -6,9 +6,11 @@ import testenv import fake_env from pubs import pubs_cmd -from pubs import color, content, filebroker, uis, beets_ui, p3 +from pubs import color, content, filebroker, uis, beets_ui, p3, endecoder import str_fixtures +import fixtures + from pubs.commands import init_cmd, import_cmd @@ -33,7 +35,7 @@ class TestFakeInput(unittest.TestCase): color.input() def test_editor_input(self): - other_input = fake_env.FakeInput(['yes', 'no'], + other_input = fake_env.FakeInput(['yes', 'no'], module_list=[content, color]) other_input.as_global() self.assertEqual(content.editor_input(), 'yes') @@ -112,13 +114,13 @@ class TestAdd(DataCommandTestCase): def test_add(self): cmds = ['pubs init', - 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', + 'pubs add /data/pagerank.bib -d /data/pagerank.pdf', ] self.execute_cmds(cmds) def test_add2(self): cmds = ['pubs init -p /not_default', - 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', + 'pubs add /data/pagerank.bib -d /data/pagerank.pdf', ] self.execute_cmds(cmds) self.assertEqual(set(self.fs['os'].listdir('/not_default/doc')), {'Page99.pdf'}) @@ -129,7 +131,7 @@ class TestList(DataCommandTestCase): def test_list(self): cmds = ['pubs init -p /not_default2', 'pubs list', - 'pubs add -b /data/pagerank.bib -d /data/pagerank.pdf', + 'pubs add /data/pagerank.bib -d /data/pagerank.pdf', 'pubs list', ] self.execute_cmds(cmds) @@ -177,7 +179,7 @@ class TestUsecase(DataCommandTestCase): ] cmds = ['pubs init -p paper_first/', - 'pubs add -d data/pagerank.pdf -b data/pagerank.bib', + 'pubs add -d data/pagerank.pdf data/pagerank.bib', 'pubs list', 'pubs tag', 'pubs tag Page99 network+search', @@ -189,10 +191,10 @@ class TestUsecase(DataCommandTestCase): def test_second(self): cmds = ['pubs init -p paper_second/', - 'pubs add -b data/pagerank.bib', - 'pubs add -d data/turing-mind-1950.pdf -b data/turing1950.bib', - 'pubs add -b data/martius.bib', - 'pubs add -b data/10.1371%2Fjournal.pone.0038236.bib', + 'pubs add data/pagerank.bib', + 'pubs add -d data/turing-mind-1950.pdf data/turing1950.bib', + 'pubs add data/martius.bib', + 'pubs add data/10.1371%2Fjournal.pone.0038236.bib', 'pubs list', 'pubs attach Page99 data/pagerank.pdf' ] @@ -200,10 +202,10 @@ class TestUsecase(DataCommandTestCase): def test_third(self): cmds = ['pubs init', - 'pubs add -b data/pagerank.bib', - 'pubs add -d data/turing-mind-1950.pdf -b data/turing1950.bib', - 'pubs add -b data/martius.bib', - 'pubs add -b data/10.1371%2Fjournal.pone.0038236.bib', + 'pubs add data/pagerank.bib', + 'pubs add -d data/turing-mind-1950.pdf data/turing1950.bib', + 'pubs add data/martius.bib', + 'pubs add data/10.1371%2Fjournal.pone.0038236.bib', 'pubs list', 'pubs attach Page99 data/pagerank.pdf', ('pubs remove Page99', ['y']), @@ -216,7 +218,7 @@ class TestUsecase(DataCommandTestCase): cmds = ['pubs init', ('pubs add', ['abc', 'n']), ('pubs add', ['abc', 'y', 'abc', 'n']), - 'pubs add -b data/pagerank.bib', + 'pubs add data/pagerank.bib', ('pubs edit Page99', ['', 'a']), ] self.execute_cmds(cmds) @@ -240,7 +242,7 @@ class TestUsecase(DataCommandTestCase): line3 = re.sub('Page99', 'Ridge07', line2) cmds = ['pubs init', - 'pubs add -b data/pagerank.bib', + 'pubs add data/pagerank.bib', ('pubs list', [], line), ('pubs edit Page99', [bib1]), ('pubs list', [], line1), @@ -256,11 +258,12 @@ class TestUsecase(DataCommandTestCase): cmds = ['pubs init', ('pubs add', [str_fixtures.bibtex_external0]), 'pubs export Page99', - ('pubs export Page99 -f bibtex', [], str_fixtures.bibtex_raw0), + ('pubs export Page99 -f bibtex', []), 'pubs export Page99 -f bibyaml', ] - self.execute_cmds(cmds) + outs = self.execute_cmds(cmds) + self.assertEqual(endecoder.EnDecoder().decode_bibdata(outs[3]), fixtures.page_bibdata) def test_import(self): cmds = ['pubs init', @@ -282,7 +285,7 @@ class TestUsecase(DataCommandTestCase): def test_open(self): cmds = ['pubs init', - 'pubs add -b data/pagerank.bib', + 'pubs add data/pagerank.bib', 'pubs open Page99' ] @@ -295,7 +298,7 @@ class TestUsecase(DataCommandTestCase): def test_update(self): cmds = ['pubs init', - 'pubs add -b data/pagerank.bib', + 'pubs add data/pagerank.bib', 'pubs update' ] From dfd16c029d89e12962a18ccfaaa9a88dd582d2f7 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 14:53:43 +0100 Subject: [PATCH 39/48] first working paper test + bugfix --- pubs/paper.py | 6 ++- tests/fixtures.py | 3 +- tests/str_fixtures.py | 2 +- tests/test_paper.py | 100 +++--------------------------------------- 4 files changed, 12 insertions(+), 99 deletions(-) diff --git a/pubs/paper.py b/pubs/paper.py index 7f6c345..e074356 100644 --- a/pubs/paper.py +++ b/pubs/paper.py @@ -28,9 +28,11 @@ class Paper(object): if self.metadata is None: self.metadata = copy.deepcopy(DEFAULT_META) if self.citekey is None: - self.citekey = bibstruct.extract_citekey(self.bibdata) + self.citekey = bibstruct.extract_citekey(self.bibdata) bibstruct.check_citekey(self.citekey) + self.metadata['tags'] = set(self.metadata.get('tags', [])) + def __eq__(self, other): return (isinstance(self, Paper) and type(other) is type(self) and self.bibdata == other.bibdata @@ -61,7 +63,7 @@ class Paper(object): @property def tags(self): - return self.metadata.setdefault('tags', set()) + return self.metadata['tags'] @tags.setter def tags(self, value): diff --git a/tests/fixtures.py b/tests/fixtures.py index 73bdac0..2644ff1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -23,5 +23,6 @@ doe_bib = """ franny_bibdata = coder.decode_bibdata(franny_bib) doe_bibdata = coder.decode_bibdata(doe_bib) -page_bibdata = coder.decode_bibdata(str_fixtures.bibtex_raw0) turing_bibdata = coder.decode_bibdata(str_fixtures.turing_bib) +page_bibdata = coder.decode_bibdata(str_fixtures.bibtex_raw0) +page_metadata = coder.decode_metadata(str_fixtures.metadata_raw0) \ No newline at end of file diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index eebfc97..4026465 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -99,7 +99,7 @@ bibtex_raw0 = """@techreport{ """ -metadata_raw0 = """docfile: null +metadata_raw0 = """docfile: docsdir://Page99.pdf tags: [search, network] added: '2013-11-14 13:14:20' """ diff --git a/tests/test_paper.py b/tests/test_paper.py index 87cc8eb..92e0723 100644 --- a/tests/test_paper.py +++ b/tests/test_paper.py @@ -1,106 +1,16 @@ # -*- coding: utf-8 -*- import os import unittest -import tempfile -import shutil - -import yaml -from pybtex.database import Person import testenv import fixtures -from papers.paper import Paper - - -BIB = """ -entries: - Turing1950: - author: - - first: 'Alan' - last: 'Turing' - title: 'Computing machinery and intelligence.' - type: article - year: '1950' -""" -META = """ -external-document: null -notes: [] -tags: ['AI', 'computer'] -""" - - -class TestCreateCitekey(unittest.TestCase): - - def test_fails_on_empty_paper(self): - paper = Paper() - with self.assertRaises(ValueError): - paper.generate_citekey() - - -class TestSaveLoad(unittest.TestCase): - - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - os.makedirs(os.path.join(self.tmpdir, 'bibdata')) - os.makedirs(os.path.join(self.tmpdir, 'meta')) - self.bibfile = os.path.join(self.tmpdir, 'bib.bibyaml') - with open(self.bibfile, 'w') as f: - f.write(BIB) - self.metafile = os.path.join(self.tmpdir, 'meta.meta') - with open(self.metafile, 'w') as f: - f.write(META) - self.dest_bibfile = os.path.join(self.tmpdir, 'written_bib.yaml') - self.dest_metafile = os.path.join(self.tmpdir, 'written_meta.yaml') - - def test_load_valid(self): - p = Paper.load(self.bibfile, metapath=self.metafile) - self.assertEqual(fixtures.turing1950, p) - - def test_save_fails_with_no_citekey(self): - p = Paper() - with self.assertRaises(ValueError): - p.save(self.dest_bibfile, self.dest_metafile) - - def test_save_creates_bib(self): - fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) - self.assertTrue(os.path.exists(self.dest_bibfile)) - - def test_save_creates_meta(self): - fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) - self.assertTrue(os.path.exists(self.dest_metafile)) - - def test_save_right_bib(self): - fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) - with open(self.dest_bibfile, 'r') as f: - written = yaml.load(f) - ok = yaml.load(BIB) - self.assertEqual(written, ok) - - def test_save_right_meta(self): - fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) - with open(self.dest_metafile, 'r') as f: - written = yaml.load(f) - ok = yaml.load(META) - self.assertEqual(written, ok) - - def tearDown(self): - shutil.rmtree(self.tmpdir) +from pubs.paper import Paper -class TestCopy(unittest.TestCase): +class TestAttributes(unittest.TestCase): - def setUp(self): - self.orig = Paper() - self.orig.bibentry.fields['title'] = u'Nice title.' - self.orig.bibentry.fields['year'] = u'2013' - self.orig.bibentry.persons['author'] = [Person(u'John Doe')] - self.orig.citekey = self.orig.generate_citekey() + def test_tags(self): + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata) + self.assertEqual(p.tags, set(['search', 'network'])) - def test_copy_equal(self): - copy = self.orig.copy() - self.assertEqual(copy, self.orig) - def test_copy_can_be_changed(self): - copy = self.orig.copy() - copy.bibentry.fields['year'] = 2014 - self.assertEqual(self.orig.bibentry.fields['year'], u'2013') From 4ce261d63690ce27dc11e1ff7cca2905e91b3b2f Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 15:04:34 +0100 Subject: [PATCH 40/48] more tests on tags --- tests/test_paper.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_paper.py b/tests/test_paper.py index 92e0723..9f5c3b3 100644 --- a/tests/test_paper.py +++ b/tests/test_paper.py @@ -10,7 +10,34 @@ from pubs.paper import Paper class TestAttributes(unittest.TestCase): def test_tags(self): - p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata) + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata).deepcopy() self.assertEqual(p.tags, set(['search', 'network'])) + def test_add_tag(self): + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata).deepcopy() + p.add_tag('algorithm') + self.assertEqual(p.tags, set(['search', 'network', 'algorithm'])) + p.add_tag('algorithm') + self.assertEqual(p.tags, set(['search', 'network', 'algorithm'])) + def test_set_tags(self): + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata).deepcopy() + p.tags = ['algorithm'] + self.assertEqual(p.tags, set(['algorithm'])) + + def test_remove_tags(self): + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata).deepcopy() + p.remove_tag('network') + self.assertEqual(p.tags, set(['search'])) + + def test_mixed_tags(self): + p = Paper(fixtures.page_bibdata, metadata=fixtures.page_metadata).deepcopy() + p.add_tag('algorithm') + self.assertEqual(p.tags, set(['search', 'network', 'algorithm'])) + p.remove_tag('network') + self.assertEqual(p.tags, set(['search', 'algorithm'])) + p.tags = ['ranking'] + self.assertEqual(p.tags, set(['ranking'])) + p.remove_tag('ranking') + self.assertEqual(p.tags, set()) + p.remove_tag('ranking') From 7fe77046990ee655d74914ec1b72de18c406f877 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 19:05:41 +0100 Subject: [PATCH 41/48] fix attach_cmd where meta would not be updated --- pubs/commands/attach_cmd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pubs/commands/attach_cmd.py b/pubs/commands/attach_cmd.py index a3caf52..6ffed16 100644 --- a/pubs/commands/attach_cmd.py +++ b/pubs/commands/attach_cmd.py @@ -11,7 +11,7 @@ def parser(subparsers): help="don't copy document files (opposite of -c)") parser.add_argument('citekey', help='citekey of the paper') - parser.add_argument('document', + parser.add_argument('document', help='document file') return parser @@ -38,6 +38,7 @@ def command(args): else: pass # TODO warn if file does not exists paper.docpath = document + rp.push_paper(paper, overwrite=True, event=False) except ValueError, v: ui.error(v.message) ui.exit(1) From 3aaed155f6dac3a8782d5e781076b272cc1b2068 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 19:29:51 +0100 Subject: [PATCH 42/48] pubs list returns papers sorted by the time they were added --- pubs/commands/list_cmd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pubs/commands/list_cmd.py b/pubs/commands/list_cmd.py index 64956ca..228cf31 100644 --- a/pubs/commands/list_cmd.py +++ b/pubs/commands/list_cmd.py @@ -22,6 +22,10 @@ def parser(subparsers): return parser +def date_added(np): + n, p = np + return p.metadata['added'] + def command(args): ui = get_ui() rp = repo.Repository(config()) @@ -30,7 +34,7 @@ def command(args): enumerate(rp.all_papers())) ui.print_('\n'.join( pretty.paper_oneliner(p, n=n, citekey_only=args.citekeys) - for n, p in papers)) + for n, p in sorted(papers, key=date_added))) FIELD_ALIASES = { From be88ab1cf56d6b09991f942d211f98b8fc2b4fe0 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 20:41:16 +0100 Subject: [PATCH 43/48] added 'pdf' field to docfile search in bibfile --- pubs/bibstruct.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubs/bibstruct.py b/pubs/bibstruct.py index 3c086df..087923a 100644 --- a/pubs/bibstruct.py +++ b/pubs/bibstruct.py @@ -81,5 +81,7 @@ def extract_docfile(bibdata, remove=False): return f if 'attachments' in entry.fields: return entry.fields['attachments'] + if 'pdf' in entry.fields: + return entry.fields['pdf'] except (KeyError, IndexError): return None From e6d4c338463840bd76b9eed71909d0650bd71fdf Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 20:52:39 +0100 Subject: [PATCH 44/48] added dialog when downloading a file --- pubs/content.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pubs/content.py b/pubs/content.py index da42d2f..7197889 100644 --- a/pubs/content.py +++ b/pubs/content.py @@ -7,6 +7,8 @@ import urlparse import httplib import urllib2 +import uis + # files i/o @@ -35,7 +37,7 @@ def read_file(filepath): with open(filepath, 'r') as f: s = f.read() return s - + def write_file(filepath, data): check_directory(os.path.dirname(filepath)) with open(filepath, 'w') as f: @@ -59,7 +61,7 @@ def url_exists(url): conn.close() return response.status == 200 - + def check_content(path): if content_type(path) == 'url': return url_exists(path) @@ -69,6 +71,7 @@ def check_content(path): def get_content(path): """Will be useful when we need to get content from url""" if content_type(path) == 'url': + uis.get_ui().print_('dowloading {}'.format(path)) response = urllib2.urlopen(path) return response.read() else: From 523fe888a3e37edc0c1b73e45c1dd1eba93c85f5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 18 Nov 2013 21:03:47 +0100 Subject: [PATCH 45/48] many bugfixes when adding paper through editor --- pubs/commands/add_cmd.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index d892684..23bb832 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -27,27 +27,29 @@ def parser(subparsers): def bibdata_from_editor(ui, rp): again = True - try: - bibstr = content.editor_input(config().edit_cmd, - templates.add_bib, - suffix='.bib') - if bibstr == templates.add_bib: - cont = ui.input_yn( - question='Bibfile not edited. Edit again ?', + bibstr = templates.add_bib + while again: + try: + bibstr = content.editor_input(config().edit_cmd, + bibstr, + suffix='.bib') + if bibstr == templates.add_bib: + again = ui.input_yn( + question='Bibfile not edited. Edit again ?', + default='y') + if not again: + ui.exit(0) + else: + bibdata = rp.databroker.verify(bibstr) + bibstruct.verify_bibdata(bibdata) + # REFACTOR Generate citykey + again = False + except ValueError: + again = ui.input_yn( + question='Invalid bibfile. Edit again ?', default='y') - if not cont: + if not again: ui.exit(0) - else: - bibdata = rp.databroker.verify(bibstr) - bibstruct.verify_bibdata(bibdata) - # REFACTOR Generate citykey - cont = False - except ValueError: - again = ui.input_yn( - question='Invalid bibfile. Edit again ?', - default='y') - if not again: - ui.exit(0) return bibdata From c692f23054511c52a14cf4c1b5b6ca73372494f8 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Tue, 19 Nov 2013 00:48:53 +0100 Subject: [PATCH 46/48] space separation for tags + tag color --- pubs/color.py | 1 + pubs/commands/tag_cmd.py | 26 +++++++++++++++++++------- pubs/pretty.py | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pubs/color.py b/pubs/color.py index ada8dec..d645b81 100644 --- a/pubs/color.py +++ b/pubs/color.py @@ -20,6 +20,7 @@ error = red normal = grey citekey = purple filepath = cyan +tag = blue def dye(s, color=end, bold=False): assert color[0] == '\033' diff --git a/pubs/commands/tag_cmd.py b/pubs/commands/tag_cmd.py index d978eb1..ea1fc8f 100644 --- a/pubs/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -17,25 +17,33 @@ The different use cases are : display all papers with the tag 'math', 'romance' but not 'war' """ +import re + from ..repo import Repository, InvalidReference from ..configs import config from ..uis import get_ui from .. import pretty +from .. import color def parser(subparsers): parser = subparsers.add_parser('tag', help="add, remove and show tags") parser.add_argument('citekeyOrTag', nargs='?', default = None, help='citekey or tag.') - parser.add_argument('tags', nargs='?', default = None, + parser.add_argument('tags', nargs='*', default = None, help='If the previous argument was a citekey, then ' 'then a list of tags separated by a +.') # TODO find a way to display clear help for multiple command semantics, # indistinguisable for argparse. (fabien, 201306) return parser +def _parse_tags(list_tags): + """Transform 'math-ai network -search' in ['+math', '-ai', '+network', '-search']""" + tags = [] + for s in list_tags: + tags += _parse_tag_seq(s) + return tags -import re -def _parse_tags(s): +def _parse_tag_seq(s): """Transform 'math-ai' in ['+math', '-ai']""" tags = [] if s[0] not in ['+', '-']: @@ -76,12 +84,14 @@ def command(args): if citekeyOrTag is None: for tag in rp.get_tags(): - ui.print_(tag) + ui.print_(color.dye(' '.join(rp.get_tags()), + color=color.blue)) else: if rp.databroker.exists(citekeyOrTag): p = rp.pull_paper(citekeyOrTag) - if tags is None: - ui.print_(' '.join(p.tags)) + if tags == []: + ui.print_(color.dye(' '.join(p.tags), + color=color.blue)) else: add_tags, remove_tags = _tag_groups(_parse_tags(tags)) for tag in add_tags: @@ -91,7 +101,9 @@ def command(args): rp.push_paper(p, overwrite=True) else: # case where we want to find papers with specific tags - included, excluded = _tag_groups(_parse_tags(citekeyOrTag)) + all_tags = [citekeyOrTag] + all_tags += tags + included, excluded = _tag_groups(_parse_tags(all_tags)) papers_list = [] for n, p in enumerate(rp.all_papers()): if (p.tags.issuperset(included) and diff --git a/pubs/pretty.py b/pubs/pretty.py index 8b79da2..8a107dd 100644 --- a/pubs/pretty.py +++ b/pubs/pretty.py @@ -61,5 +61,5 @@ def paper_oneliner(p, n = 0, citekey_only = False): citekey=color.dye(p.citekey, color.purple), descr=bibdesc, tags=color.dye(' '.join(p.tags), - color.purple, bold=True), + color.tag, bold=False), )).encode('utf-8') \ No newline at end of file From d3736e257be2e184e9b8140502803aebf653a92f Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Sun, 13 Apr 2014 15:10:40 +0200 Subject: [PATCH 47/48] moving from pybtex to bibtextparser, fixed major regressions. * only bibtex format is supported * all tests except test_repo.py and edit test pass * edit and update commands were not updated * removed --format argument from export, only bibtex is supported. --- .ackrc | 1 + NOTES | 3 +- pubs/__init__.py | 2 +- pubs/bibstruct.py | 35 +++--- pubs/commands/add_cmd.py | 2 +- pubs/commands/attach_cmd.py | 2 +- pubs/commands/edit_cmd.py | 21 +++- pubs/commands/export_cmd.py | 15 +-- pubs/commands/import_cmd.py | 13 +- pubs/commands/list_cmd.py | 21 ++-- pubs/datacache.py | 22 ++-- pubs/endecoder.py | 115 ++++++++++++------ pubs/filebroker.py | 26 ++-- pubs/paper.py | 16 ++- pubs/pretty.py | 37 +++--- pubs/repo.py | 17 ++- README.md => readme.md | 15 ++- setup.py | 2 +- tests/{zoo => bibexamples}/incollections.bib | 0 tests/{testenv.py => dotdot.py} | 0 tests/fake_env.py | 2 +- tests/fixtures.py | 24 ++-- tests/str_fixtures.py | 73 +---------- tests/test_bibstruct.py | 10 +- tests/test_color.py | 4 +- tests/test_config.py | 6 +- tests/test_databroker.py | 27 ++-- tests/test_endecoder.py | 40 +++--- tests/test_events.py | 10 +- tests/test_filebroker.py | 14 ++- tests/test_paper.py | 6 +- tests/test_pretty.py | 21 ++++ tests/test_queries.py | 86 +++++++------ tests/test_repo.py | 75 +++++++++++- tests/test_tag.py | 20 +-- tests/test_usecase.py | 46 ++++--- .../bib/10.1371_journal.pone.0038236.bib | 15 +++ .../bib/10.1371_journal.pone.0038236.bibyaml | 45 ------- .../bib/10.1371journal.pone.0063400.bib | 15 +++ .../bib/10.1371journal.pone.0063400.bibyaml | 36 ------ tests/testrepo/bib/Page99.bib | 13 ++ tests/testrepo/bib/Page99.bibyaml | 28 ----- tests/testrepo/bib/journal0063400.bib | 6 + tests/testrepo/bib/journal0063400.bibyaml | 15 --- 44 files changed, 530 insertions(+), 472 deletions(-) rename README.md => readme.md (61%) rename tests/{zoo => bibexamples}/incollections.bib (100%) rename tests/{testenv.py => dotdot.py} (100%) create mode 100644 tests/test_pretty.py create mode 100644 tests/testrepo/bib/10.1371_journal.pone.0038236.bib delete mode 100644 tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml create mode 100644 tests/testrepo/bib/10.1371journal.pone.0063400.bib delete mode 100644 tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml create mode 100644 tests/testrepo/bib/Page99.bib delete mode 100644 tests/testrepo/bib/Page99.bibyaml create mode 100644 tests/testrepo/bib/journal0063400.bib delete mode 100644 tests/testrepo/bib/journal0063400.bibyaml diff --git a/.ackrc b/.ackrc index 1617d11..a61f58e 100644 --- a/.ackrc +++ b/.ackrc @@ -1 +1,2 @@ --ignore-directory=is:build +--ignore-directory=is:pubs.egg-info diff --git a/NOTES b/NOTES index 76dd465..ab4ed53 100644 --- a/NOTES +++ b/NOTES @@ -14,13 +14,12 @@ A paper correspond to 3 files : About strings: -------------- -- pybtex seems to store entries as utf-8 (TODO: check) - so assumption is made that everything is utf-8 - conversions are performed at print time Config values: -------------- -[papers] +[pubs] open-cmd = open edit-cmd = edit import-copy = True diff --git a/pubs/__init__.py b/pubs/__init__.py index b081b7c..b350a5d 100644 --- a/pubs/__init__.py +++ b/pubs/__init__.py @@ -1 +1 @@ -__version__ = 4 \ No newline at end of file +__version__ = 4 diff --git a/pubs/bibstruct.py b/pubs/bibstruct.py index 087923a..1d46345 100644 --- a/pubs/bibstruct.py +++ b/pubs/bibstruct.py @@ -21,20 +21,25 @@ def check_citekey(citekey): raise ValueError("Invalid citekey: %s" % citekey) def verify_bibdata(bibdata): - if not hasattr(bibdata, 'entries') or len(bibdata.entries) == 0: - raise ValueError('no entries in the bibdata.') - if len(bibdata.entries) > 1: + if bibdata is None or len(bibdata) == 0: + raise ValueError('no valid bibdata') + if len(bibdata) > 1: raise ValueError('ambiguous: multiple entries in the bibdata.') def get_entry(bibdata): verify_bibdata(bibdata) - return bibdata.entries.iteritems().next() + for e in bibdata.items(): + return e def extract_citekey(bibdata): verify_bibdata(bibdata) citekey, entry = get_entry(bibdata) return citekey +def author_last(author_str): + """ Return the last name of the author """ + return author_str.split(',')[0] + def generate_citekey(bibdata): """ Generate a citekey from bib_data. @@ -44,17 +49,17 @@ def generate_citekey(bibdata): """ citekey, entry = get_entry(bibdata) - author_key = 'author' if 'author' in entry.persons else 'editor' + author_key = 'author' if 'author' in entry else 'editor' try: - first_author = entry.persons[author_key][0] + first_author = entry[author_key][0] except KeyError: raise ValueError( 'No author or editor defined: cannot generate a citekey.') try: - year = entry.fields['year'] + year = entry['year'] except KeyError: year = '' - citekey = u'{}{}'.format(u''.join(first_author.last()), year) + citekey = u'{}{}'.format(u''.join(author_last(first_author)), year) return str2citekey(citekey) @@ -67,21 +72,21 @@ def extract_docfile(bibdata, remove=False): citekey, entry = get_entry(bibdata) try: - if 'file' in entry.fields: - field = entry.fields['file'] + if 'file' in entry: + field = entry['file'] # Check if this is mendeley specific for f in field.split(':'): if len(f) > 0: break if remove: - entry.fields.pop('file') + entry.pop('file') # This is a hck for Mendeley. Make clean if f[0] != '/': f = '/' + f return f - if 'attachments' in entry.fields: - return entry.fields['attachments'] - if 'pdf' in entry.fields: - return entry.fields['pdf'] + if 'attachments' in entry: + return entry['attachments'] + if 'pdf' in entry: + return entry['pdf'] except (KeyError, IndexError): return None diff --git a/pubs/commands/add_cmd.py b/pubs/commands/add_cmd.py index 23bb832..6dfc5ce 100644 --- a/pubs/commands/add_cmd.py +++ b/pubs/commands/add_cmd.py @@ -108,7 +108,7 @@ def command(args): if copy_doc is None: copy_doc = config().import_copy if copy_doc: - docfile = rp.databroker.copy_doc(citekey, docfile) + docfile = rp.databroker.add_doc(citekey, docfile) # create the paper diff --git a/pubs/commands/attach_cmd.py b/pubs/commands/attach_cmd.py index 6ffed16..dc5e6bf 100644 --- a/pubs/commands/attach_cmd.py +++ b/pubs/commands/attach_cmd.py @@ -34,7 +34,7 @@ def command(args): try: document = args.document if copy: - document = rp.databroker.copy_doc(paper.citekey, document) + document = rp.databroker.add_doc(paper.citekey, document) else: pass # TODO warn if file does not exists paper.docpath = document diff --git a/pubs/commands/edit_cmd.py b/pubs/commands/edit_cmd.py index 676e545..ae703fd 100644 --- a/pubs/commands/edit_cmd.py +++ b/pubs/commands/edit_cmd.py @@ -15,6 +15,24 @@ def parser(subparsers): return parser +def edit_meta(citekey): + rp = repo.Repository(config()) + coder = endecoder.EnDecoder() + filepath = os.path.join(rp.databroker.databroker.filebroker.metadir(), citekey+'.yaml') + with open(filepath) as f: + content = f.read() + + + +def edit_bib(citekey): + rp = repo.Repository(config()) + coder = endecoder.EnDecoder() + filepath = os.path.join(rp.databroker.databroker.filebroker.bibdir(), citekey+'.bib') + with open(filepath) as f: + content = f.read() + + + def command(args): ui = get_ui() @@ -26,8 +44,7 @@ def command(args): if meta: filepath = os.path.join(rp.databroker.databroker.filebroker.metadir(), citekey+'.yaml') else: - filepath = os.path.join(rp.databroker.databroker.filebroker.bibdir(), citekey+'.bibyaml') - + filepath = os.path.join(rp.databroker.databroker.filebroker.bibdir(), citekey+'.bib') with open(filepath) as f: content = f.read() diff --git a/pubs/commands/export_cmd.py b/pubs/commands/export_cmd.py index 1a729ea..cab8201 100644 --- a/pubs/commands/export_cmd.py +++ b/pubs/commands/export_cmd.py @@ -1,8 +1,6 @@ from __future__ import print_function import sys -from pybtex.database import BibliographyData - from .. import repo from ..configs import config from ..uis import get_ui @@ -11,8 +9,8 @@ from .. import endecoder def parser(subparsers): parser = subparsers.add_parser('export', help='export bibliography') - parser.add_argument('-f', '--bib-format', default='bibtex', - help='export format') + # parser.add_argument('-f', '--bib-format', default='bibtex', + # help='export format') parser.add_argument('citekeys', nargs='*', help='one or several citekeys') return parser @@ -20,11 +18,10 @@ def parser(subparsers): def command(args): """ - :param bib_format (in 'bibtex', 'yaml') """ + # :param bib_format (only 'bibtex' now) ui = get_ui() - bib_format = args.bib_format rp = repo.Repository(config()) @@ -36,12 +33,12 @@ def command(args): if len(papers) == 0: papers = rp.all_papers() - bib = BibliographyData() + bib = {} for p in papers: - bib.add_entry(p.citekey, p.bibentry) + bib[p.citekey] = p.bibentry try: exporter = endecoder.EnDecoder() - bibdata_raw = exporter.encode_bibdata(bib, fmt=bib_format) + bibdata_raw = exporter.encode_bibdata(bib) print(bibdata_raw, end='') except KeyError: ui.error("Invalid output format: %s." % bib_format) diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 717e586..fbd6939 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -1,8 +1,6 @@ import os import datetime -from pybtex.database import Entry, BibliographyData, FieldDict, Person - from .. import repo from .. import endecoder from .. import bibstruct @@ -41,8 +39,9 @@ def many_from_path(bibpath): bibpath = os.path.expanduser(bibpath) if os.path.isdir(bibpath): + print([os.path.splitext(f)[-1][1:] for f in os.listdir(bibpath)]) all_files = [os.path.join(bibpath, f) for f in os.listdir(bibpath) - if os.path.splitext(f)[-1][1:] in list(coder.decode_fmt.keys())] + if os.path.splitext(f)[-1][1:] == 'bib'] else: all_files = [bibpath] @@ -53,10 +52,10 @@ def many_from_path(bibpath): papers = {} for b in biblist: - for k in b.entries: + for k in b.keys(): try: - bibdata = BibliographyData() - bibdata.entries[k] = b.entries[k] + bibdata = {} + bibdata[k] = b[k] papers[k] = Paper(bibdata, citekey=k) papers[k].added = datetime.datetime.now() @@ -94,7 +93,7 @@ def command(args): if copy_doc is None: copy_doc = config().import_copy if copy_doc: - docfile = rp.databroker.copy_doc(p.citekey, docfile) + docfile = rp.databroker.add_doc(p.citekey, docfile) p.docpath = docfile rp.push_paper(p) diff --git a/pubs/commands/list_cmd.py b/pubs/commands/list_cmd.py index 228cf31..2acdec6 100644 --- a/pubs/commands/list_cmd.py +++ b/pubs/commands/list_cmd.py @@ -1,9 +1,9 @@ from .. import repo from .. import pretty +from .. import bibstruct from ..configs import config from ..uis import get_ui - class InvalidQuery(ValueError): pass @@ -56,20 +56,15 @@ def _get_field_value(query_block): return (field, value) -def _lower(string, lower=True): - if lower: - return string.lower() - else: - return string - +def _lower(s, lower=True): + return s.lower() if lower else s def _check_author_match(paper, query, case_sensitive=False): """Only checks within last names.""" - if not 'author' in paper.bibentry.persons: + if not 'author' in paper.bibentry: return False - return any([query in _lower(name, lower=(not case_sensitive)) - for p in paper.bibentry.persons['author'] - for name in p.last()]) + return any([query == _lower(bibstruct.author_last(p), lower=(not case_sensitive)) + for p in paper.bibentry['author']]) def _check_tag_match(paper, query, case_sensitive=False): @@ -78,7 +73,7 @@ def _check_tag_match(paper, query, case_sensitive=False): def _check_field_match(paper, field, query, case_sensitive=False): - return query in _lower(paper.bibentry.fields[field], + return query in _lower(paper.bibentry[field], lower=(not case_sensitive)) @@ -92,7 +87,7 @@ def _check_query_block(paper, query_block, case_sensitive=None): return _check_tag_match(paper, value, case_sensitive=case_sensitive) elif field == 'author': return _check_author_match(paper, value, case_sensitive=case_sensitive) - elif field in paper.bibentry.fields: + elif field in paper.bibentry: return _check_field_match(paper, field, value, case_sensitive=case_sensitive) else: diff --git a/pubs/datacache.py b/pubs/datacache.py index 8dbec1a..b4fe98a 100644 --- a/pubs/datacache.py +++ b/pubs/datacache.py @@ -4,7 +4,7 @@ from . import databroker class DataCache(object): """ DataCache class, provides a very similar interface as DataBroker - Has two roles : + Has two roles : 1. Provides a buffer between the commands and the hard drive. Until a command request a hard drive ressource, it does not touch it. 2. Keeps a up-to-date, pickled version of the repository, to speed up things @@ -12,7 +12,7 @@ class DataCache(object): Changes are detected using data modification timestamps. For the moment, only (1) is implemented. - """ + """ def __init__(self, directory, create=False): self.directory = directory self._databroker = None @@ -30,16 +30,16 @@ class DataCache(object): def pull_metadata(self, citekey): return self.databroker.pull_metadata(citekey) - + def pull_bibdata(self, citekey): return self.databroker.pull_bibdata(citekey) - + def push_metadata(self, citekey, metadata): self.databroker.push_metadata(citekey, metadata) - + def push_bibdata(self, citekey, bibdata): self.databroker.push_bibdata(citekey, bibdata) - + def push(self, citekey, metadata, bibdata): self.databroker.push(citekey, metadata, bibdata) @@ -59,23 +59,23 @@ class DataCache(object): def verify(self, bibdata_raw): """Will return None if bibdata_raw can't be decoded""" return self.databroker.verify(bibdata_raw) - + # docbroker def in_docsdir(self, docpath): return self.databroker.in_docsdir(docpath) def real_docpath(self, docpath): - return self.databroker.real_docpath(docpath) + return self.databroker.real_docpath(docpath) - def copy_doc(self, citekey, source_path, overwrite=False): + def add_doc(self, citekey, source_path, overwrite=False): return self.databroker.add_doc(citekey, source_path, overwrite=overwrite) def remove_doc(self, docpath, silent=True): return self.databroker.remove_doc(docpath, silent=silent) def rename_doc(self, docpath, new_citekey): - return self.databroker.rename_doc(docpath, new_citekey) + return self.databroker.rename_doc(docpath, new_citekey) # notesbroker @@ -94,7 +94,7 @@ class DataCache(object): # def __init__(self, cache, directory): # self.cache = cache # self.directory = directory - + # def changes(self): # """ Returns the list of modified files since the last cache was saved to disk""" # pass diff --git a/pubs/endecoder.py b/pubs/endecoder.py index 894784f..6abcdaf 100644 --- a/pubs/endecoder.py +++ b/pubs/endecoder.py @@ -1,5 +1,5 @@ -import color -import yaml +from __future__ import print_function, absolute_import, division, unicode_literals +import copy try: import cStringIO as StringIO @@ -7,19 +7,44 @@ except ImportError: import StringIO try: - import pybtex.database.input.bibtex - import pybtex.database.input.bibtexml - import pybtex.database.input.bibyaml - import pybtex.database.output.bibtex - import pybtex.database.output.bibtexml - import pybtex.database.output.bibyaml - + import bibtexparser as bp except ImportError: print(color.dye('error', color.error) + - ": you need to install Pybtex; try running 'pip install " - "pybtex' or 'easy_install pybtex'") + ": you need to install bibterxparser; try running 'pip install " + "bibtexparser'.") exit(-1) +import yaml + +from . import color + + +def sanitize_citekey(record): + record['id'] = record['id'].strip('\n') + return record + +def customizations(record): + """ Use some functions delivered by the library + + :param record: a record + :returns: -- customized record + """ + record = bp.customization.convert_to_unicode(record) + record = bp.customization.type(record) + record = bp.customization.author(record) + record = bp.customization.editor(record) + record = bp.customization.journal(record) + record = bp.customization.keyword(record) + record = bp.customization.link(record) + record = bp.customization.page_double_hyphen(record) + record = bp.customization.doi(record) + + record = sanitize_citekey(record) + + return record + +bibfield_order = ['author', 'title', 'journal', 'institution', 'publisher', 'year', 'month', 'number', 'pages', 'link', 'doi', 'id', 'note', 'abstract'] + class EnDecoder(object): """ Encode and decode content. @@ -32,45 +57,55 @@ class EnDecoder(object): * encode_bibdata will try to recognize exceptions """ - decode_fmt = {'bibtex' : pybtex.database.input.bibtex, - 'bibyaml' : pybtex.database.input.bibyaml, - 'bib' : pybtex.database.input.bibtex, - 'bibtexml': pybtex.database.input.bibtexml} - - encode_fmt = {'bibtex' : pybtex.database.output.bibtex, - 'bibyaml' : pybtex.database.output.bibyaml, - 'bib' : pybtex.database.output.bibtex, - 'bibtexml': pybtex.database.output.bibtexml} - def encode_metadata(self, metadata): return yaml.safe_dump(metadata, allow_unicode=True, encoding='UTF-8', indent = 4) - + def decode_metadata(self, metadata_raw): return yaml.safe_load(metadata_raw) - - def encode_bibdata(self, bibdata, fmt='bib'): + + def encode_bibdata(self, bibdata): """Encode bibdata """ - s = StringIO.StringIO() - EnDecoder.encode_fmt[fmt].Writer().write_stream(bibdata, s) - return s.getvalue() + return '\n'.join(self._encode_bibentry(citekey, entry) + for citekey, entry in bibdata.items()) + + @staticmethod + def _encode_field(key, value): + if key == 'link': + return ', '.join(link['url'] for link in value) + elif key == 'author': + return ' and '.join(author for author in value) + elif key == 'journal': + return value['name'] + else: + return value + + @staticmethod + def _encode_bibentry(citekey, bibentry): + bibraw = '@{}{{{},\n'.format(bibentry['type'], citekey) + bibentry = copy.copy(bibentry) + for key in bibfield_order: + if key in bibentry: + value = bibentry.pop(key) + bibraw += ' {} = {{{}}},\n'.format(key, EnDecoder._encode_field(key, value)) + for key, value in bibentry.items(): + if key != 'type': + bibraw += ' {} = {{{}}},\n'.format(key, EnDecoder._encode_field(key, value)) + bibraw += '}\n' + return bibraw def decode_bibdata(self, bibdata_raw): """""" bibdata_rawutf8 = bibdata_raw -# bibdata_rawutf8 = unicode(bibdata_raw, 'utf8') # FIXME this doesn't work - for fmt in EnDecoder.decode_fmt.values(): - try: - bibdata_stream = StringIO.StringIO(bibdata_rawutf8) - return self._decode_bibdata(bibdata_stream, fmt.Parser()) - except ValueError: - pass - raise ValueError('could not parse bibdata') + #bibdata_rawutf8 = unicode(bibdata_raw, 'utf8') # FIXME this doesn't work + bibdata_stream = StringIO.StringIO(bibdata_rawutf8) + return self._decode_bibdata(bibdata_stream) - def _decode_bibdata(self, bibdata_stream, parser): + def _decode_bibdata(self, bibdata_stream): try: - entry = parser.parse_stream(bibdata_stream) - if len(entry.entries) > 0: - return entry + entries = bp.bparser.BibTexParser(bibdata_stream, customization=customizations).get_entry_dict() + if len(entries) > 0: + return entries except Exception: - pass + import traceback + traceback.print_exc() raise ValueError('could not parse bibdata') diff --git a/pubs/filebroker.py b/pubs/filebroker.py index 37b675d..c6b1b0d 100644 --- a/pubs/filebroker.py +++ b/pubs/filebroker.py @@ -22,7 +22,7 @@ class FileBroker(object): """ def __init__(self, directory, create=False): - self.directory = directory + self.directory = directory self.metadir = os.path.join(self.directory, 'meta') self.bibdir = os.path.join(self.directory, 'bib') if create: @@ -30,7 +30,7 @@ class FileBroker(object): check_directory(self.directory) check_directory(self.metadir) check_directory(self.bibdir) - + def _create(self): if not check_directory(self.directory, fail = False): os.mkdir(self.directory) @@ -38,7 +38,7 @@ class FileBroker(object): os.mkdir(self.metadir) if not check_directory(self.bibdir, fail = False): os.mkdir(self.bibdir) - + def pull_metafile(self, citekey): filepath = os.path.join(self.metadir, citekey + '.yaml') return read_file(filepath) @@ -46,17 +46,17 @@ class FileBroker(object): def pull_bibfile(self, citekey): filepath = os.path.join(self.bibdir, citekey + '.bib') return read_file(filepath) - + def push_metafile(self, citekey, metadata): """Put content to disk. Will gladly override anything standing in its way.""" filepath = os.path.join(self.metadir, citekey + '.yaml') write_file(filepath, metadata) - + def push_bibfile(self, citekey, bibdata): """Put content to disk. Will gladly override anything standing in its way.""" filepath = os.path.join(self.bibdir, citekey + '.bib') write_file(filepath, bibdata) - + def push(self, citekey, metadata, bibdata): """Put content to disk. Will gladly override anything standing in its way.""" self.push_metafile(citekey, metadata) @@ -72,10 +72,10 @@ class FileBroker(object): def exists(self, citekey, both=True): if both: - return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) and + return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) and check_file(os.path.join(self.bibdir, citekey + '.bib'), fail=False)) else: - return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) or + return (check_file(os.path.join(self.metadir, citekey + '.yaml'), fail=False) or check_file(os.path.join(self.bibdir, citekey + '.bib'), fail=False)) @@ -131,9 +131,9 @@ class DocBroker(object): # return check_file(os.path.join(self.docdir, citekey + ext), fail=False) def real_docpath(self, docpath): - """Return the full path + """ Return the full path Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}. - Return absoluted paths of regular ones otherwise. + Return absoluted paths of regular ones otherwise. """ if self.in_docsdir(docpath): parsed = urlparse.urlparse(docpath) @@ -160,7 +160,7 @@ class DocBroker(object): full_target_path = self.real_docpath(target_path) if not overwrite and check_file(full_target_path, fail=False): raise IOError('{} file exists.'.format(full_target_path)) - + doc_content = get_content(full_source_path) with open(full_target_path, 'wb') as f: f.write(doc_content) @@ -169,7 +169,7 @@ class DocBroker(object): def remove_doc(self, docpath, silent=True): """ Will remove only file hosted in docsdir:// - + :raise ValueError: for other paths, unless :param silent: is True """ if not self.in_docsdir(docpath): @@ -196,4 +196,4 @@ class DocBroker(object): new_docpath = self.add_doc(new_citekey, docpath) self.remove_doc(docpath) - return new_docpath \ No newline at end of file + return new_docpath diff --git a/pubs/paper.py b/pubs/paper.py index e074356..1b0a8ba 100644 --- a/pubs/paper.py +++ b/pubs/paper.py @@ -11,7 +11,7 @@ class Paper(object): """ Paper class. The object is not responsible of any disk I/O. - self.bibdata is a pybtex.database.BibliographyData object + self.bibdata is a dictionary of bibligraphic fields self.metadata is a dictionary The paper class provides methods to access the fields for its metadata @@ -43,10 +43,18 @@ class Paper(object): return 'Paper(%s, %s, %s)' % ( self.citekey, self.bibentry, self.metadata) - def deepcopy(self): + def __deepcopy__(self, memo): + return Paper(citekey =self.citekey, + metadata=copy.deepcopy(self.metadata, memo), + bibdata=copy.deepcopy(self.bibdata, memo)) + + def __copy__(self): return Paper(citekey =self.citekey, - metadata=copy.deepcopy(self.metadata), - bibdata=copy.deepcopy(self.bibdata)) + metadata=self.metadata, + bibdata=self.bibdata) + + def deepcopy(self): + return self.__deepcopy__({}) # docpath diff --git a/pubs/pretty.py b/pubs/pretty.py index 8a107dd..e8fd3ac 100644 --- a/pubs/pretty.py +++ b/pubs/pretty.py @@ -1,23 +1,19 @@ # display formatting from . import color -from pybtex.bibtex.utils import bibtex_purify -# A bug in pybtex makes the abbreviation wrong here -# (Submitted with racker ID: ID: 3605659) -# The purification should also be applied to names but unfortunately -# it removes dots which is annoying on abbreviations. +# should be adaptated to bibtexparser dicts def person_repr(p): + raise NotImplementedError return ' '.join(s for s in [ ' '.join(p.first(abbr=True)), ' '.join(p.last(abbr=False)), ' '.join(p.lineage(abbr=True))] if s) - def short_authors(bibentry): try: - authors = [person_repr(p) for p in bibentry.persons['author']] + authors = [p for p in bibentry['author']] if len(authors) < 3: return ', '.join(authors) else: @@ -28,27 +24,26 @@ def short_authors(bibentry): def bib_oneliner(bibentry): authors = short_authors(bibentry) - title = bibtex_purify(bibentry.fields['title']) - year = bibtex_purify(bibentry.fields.get('year', '')) - journal = '' - field = 'journal' - if bibentry.type == 'inproceedings': - field = 'booktitle' - journal = bibtex_purify(bibentry.fields.get(field, '')) + journal, journal_field = '', 'journal' + if 'journal' in bibentry: + journal = bibentry['journal']['name'] + elif bibentry['type'] == 'inproceedings': + journal = bibentry.get('booktitle', '') + return u'{authors} \"{title}\" {journal} ({year})'.format( authors=color.dye(authors, color.cyan), - title=title, + title=bibentry['title'], journal=color.dye(journal, color.yellow), - year=year, + year=bibentry['year'], ) def bib_desc(bib_data): - article = bib_data.entries[list(bib_data.entries.keys())[0]] - s = '\n'.join('author: {}'.format(person_repr(p)) - for p in article.persons['author']) + article = bib_data[list(bib_data.keys())[0]] + s = '\n'.join('author: {}'.format(p) + for p in article['author']) s += '\n' - s += '\n'.join('{}: {}'.format(k, v) for k, v in article.fields.items()) + s += '\n'.join('{}: {}'.format(k, v) for k, v in article.items()) return s @@ -62,4 +57,4 @@ def paper_oneliner(p, n = 0, citekey_only = False): descr=bibdesc, tags=color.dye(' '.join(p.tags), color.tag, bold=False), - )).encode('utf-8') \ No newline at end of file + )).encode('utf-8') diff --git a/pubs/repo.py b/pubs/repo.py index df474ba..46630b0 100644 --- a/pubs/repo.py +++ b/pubs/repo.py @@ -2,10 +2,8 @@ import shutil import glob import itertools -from pybtex.database import BibliographyData - from . import bibstruct -from . import events +from . import events from . import datacache from .paper import Paper @@ -70,7 +68,7 @@ class Repository(object): raise IOError('files using the {} citekey already exists'.format(paper.citekey)) if (not overwrite) and self.citekeys is not None and paper.citekey in self.citekeys: raise CiteKeyCollision('citekey {} already in use'.format(paper.citekey)) - + self.databroker.push_bibdata(paper.citekey, paper.bibdata) self.databroker.push_metadata(paper.citekey, paper.metadata) self.citekeys.add(paper.citekey) @@ -79,7 +77,7 @@ class Repository(object): def remove_paper(self, citekey, remove_doc=True, event=True): """ Remove a paper. Is silent if nothing needs to be done.""" - + if event: events.RemoveEvent(citekey).send() if remove_doc: @@ -89,7 +87,7 @@ class Repository(object): self.databroker.remove_doc(docpath, silent=True) self.databroker.remove_note(citekey, silent=True) except IOError: - pass # FXME: if IOError is about being unable to + pass # FXME: if IOError is about being unable to # remove the file, we need to issue an error.I self.citekeys.remove(citekey) @@ -103,11 +101,10 @@ class Repository(object): else: # check if new_citekey does not exists if self.databroker.exists(new_citekey, both=False): - raise IOError("can't rename paper to {}, conflicting files exists".format(new_citekey)) + raise IOError("can't rename paper to {}, conflicting files exists".format(new_citekey)) - # modify bibdata (__delitem__ not implementd by pybtex) - new_bibdata = BibliographyData() - new_bibdata.entries[new_citekey] = paper.bibdata.entries[old_citekey] + new_bibdata = {} + new_bibdata[new_citekey] = paper.bibdata[old_citekey] paper.bibdata = new_bibdata # move doc file if necessary diff --git a/README.md b/readme.md similarity index 61% rename from README.md rename to readme.md index cdc8c9b..50e9ca7 100644 --- a/README.md +++ b/readme.md @@ -1,11 +1,10 @@ -Papers -====== +# Pubs -Papers brings your bibliography to the command line. +Pubs brings your bibliography to the command line. -Papers organizes your bibliographic documents together with the bibliographic data associated to them and provides command line access to basic and advanced manipulation of your library. +Pubs organizes your bibliographic documents together with the bibliographic data associated to them and provides command line access to basic and advanced manipulation of your library. -Papers is built with the following principles in mind: +Pubs is built with the following principles in mind: - all papers are referenced using unique citation keys, - bibliographic data (i.e. pure bibtex information) is kept separated from metadata (including links to pdf or tags), @@ -19,14 +18,14 @@ Getting started --------------- Create your library (by default, goes to '~/.papers/'). - papers init + pubs init Import existing data from bibtex (papers will try to automatically copy documents defined as 'file' in bibtex): - papers import path/to/collection.bib + pubss import path/to/collection.bib or for bibtex containing a single file: - papers add --bibfile article.bib --docfile article.pdf + pubs add --bibfile article.bib --docfile article.pdf Authors diff --git a/setup.py b/setup.py index 3a9a9fe..0fe96ef 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup(name='pubs', author_email='fabien.benureau+inria@gmail.com', url='', description='research papers manager', - requires=['pybtex'], + requires=['bibtexparser'], packages=find_packages(), package_data={'': ['*.tex', '*.sty']}, scripts=['pubs/pubs'] diff --git a/tests/zoo/incollections.bib b/tests/bibexamples/incollections.bib similarity index 100% rename from tests/zoo/incollections.bib rename to tests/bibexamples/incollections.bib diff --git a/tests/testenv.py b/tests/dotdot.py similarity index 100% rename from tests/testenv.py rename to tests/dotdot.py diff --git a/tests/fake_env.py b/tests/fake_env.py index 09c49e3..3f69ffc 100644 --- a/tests/fake_env.py +++ b/tests/fake_env.py @@ -6,7 +6,7 @@ import unittest import pkgutil import re -import testenv +import dotdot import fake_filesystem import fake_filesystem_shutil import fake_filesystem_glob diff --git a/tests/fixtures.py b/tests/fixtures.py index 2644ff1..7ec3571 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -from pybtex.database import Person - -import testenv +import dotdot from pubs import endecoder import str_fixtures @@ -21,8 +19,18 @@ doe_bib = """ year = "2013"} """ -franny_bibdata = coder.decode_bibdata(franny_bib) -doe_bibdata = coder.decode_bibdata(doe_bib) -turing_bibdata = coder.decode_bibdata(str_fixtures.turing_bib) -page_bibdata = coder.decode_bibdata(str_fixtures.bibtex_raw0) -page_metadata = coder.decode_metadata(str_fixtures.metadata_raw0) \ No newline at end of file +franny_bibdata = coder.decode_bibdata(franny_bib) +franny_bibentry = franny_bibdata['Franny1961'] + +doe_bibdata = coder.decode_bibdata(doe_bib) +doe_bibentry = doe_bibdata['Doe2013'] + +turing_bibdata = coder.decode_bibdata(str_fixtures.turing_bib) +turing_bibentry = turing_bibdata['turing1950computing'] +turing_metadata = coder.decode_metadata(str_fixtures.turing_meta) + +page_bibdata = coder.decode_bibdata(str_fixtures.bibtex_raw0) +page_bibentry = page_bibdata['Page99'] +page_metadata = coder.decode_metadata(str_fixtures.metadata_raw0) + +page_metadata = coder.decode_metadata(str_fixtures.metadata_raw0) diff --git a/tests/str_fixtures.py b/tests/str_fixtures.py index 4026465..7274e51 100644 --- a/tests/str_fixtures.py +++ b/tests/str_fixtures.py @@ -1,71 +1,3 @@ -bibyaml_raw0 = """entries: - Page99: - abstract: The importance of a Web page is an inherently subjective matter, - which depends on the readers interests, knowledge and attitudes. But there - is still much that can be said objectively about the relative importance - of Web pages. This paper describes PageRank, a mathod for rating Web pages - objectively and mechanically, effectively measuring the human interest - and attention devoted to them. We compare PageRank to an idealized random - Web surfer. We show how to efficiently compute PageRank for large numbers - of pages. And, we show how to apply PageRank to search and to user navigation. - author: - - first: Lawrence - last: Page - - first: Sergey - last: Brin - - first: Rajeev - last: Motwani - - first: Terry - last: Winograd - institution: Stanford InfoLab - month: November - note: Previous number = SIDL-WP-1999-0120 - number: 1999-66 - publisher: Stanford InfoLab - title: 'The PageRank Citation Ranking: Bringing Order to the Web.' - type: techreport - url: http://ilpubs.stanford.edu:8090/422/ - year: '1999' -""" - -bibtexml_raw0 = """ - - - - - Stanford InfoLab - The PageRank Citation Ranking: Bringing Order to the Web. - http://ilpubs.stanford.edu:8090/422/ - The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation. - 1999-66 - November - Previous number = SIDL-WP-1999-0120 - 1999 - Stanford InfoLab - - - Lawrence - Page - - - Sergey - Brin - - - Rajeev - Motwani - - - Terry - Winograd - - - - - - -""" - bibtex_external0 = """ @techreport{Page99, number = {1999-66}, @@ -116,3 +48,8 @@ turing_bib = """@article{turing1950computing, } """ + +turing_meta = """\ +tags: [AI, computer] +added: '2013-11-14 13:14:20' +""" diff --git a/tests/test_bibstruct.py b/tests/test_bibstruct.py index eb1bfb1..26e116d 100644 --- a/tests/test_bibstruct.py +++ b/tests/test_bibstruct.py @@ -3,9 +3,7 @@ import os import unittest import copy -from pybtex.database import Person - -import testenv +import dotdot from pubs import bibstruct import fixtures @@ -20,7 +18,7 @@ class TestGenerateCitekey(unittest.TestCase): def test_escapes_chars(self): doe_bibdata = copy.deepcopy(fixtures.doe_bibdata) citekey, entry = bibstruct.get_entry(doe_bibdata) - entry.persons['author'] = [Person(string=u'Zôu\\@/ , John')] + entry['author'] = [u'Zôu\\@/ , John'] key = bibstruct.generate_citekey(doe_bibdata) def test_simple(self): @@ -31,3 +29,7 @@ class TestGenerateCitekey(unittest.TestCase): bibdata = copy.deepcopy(fixtures.franny_bibdata) key = bibstruct.generate_citekey(bibdata) self.assertEqual(key, 'Salinger1961') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_color.py b/tests/test_color.py index 6c08b28..58ba672 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -1,4 +1,4 @@ -import testenv +import dotdot from pubs import color def perf_color(): @@ -7,4 +7,4 @@ def perf_color(): color.dye(s, color.red) if __name__ == '__main__': - perf_color() \ No newline at end of file + perf_color() diff --git a/tests/test_config.py b/tests/test_config.py index 93f6bb1..6dbe176 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import unittest -import testenv +import dotdot from pubs import configs from pubs.configs import config from pubs.p3 import configparser @@ -67,3 +67,7 @@ class TestConfig(unittest.TestCase): def test_keywords(self): a = configs.Config(pubs_dir = '/blabla') self.assertEqual(a.pubs_dir, '/blabla') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_databroker.py b/tests/test_databroker.py index daab329..22c30cc 100644 --- a/tests/test_databroker.py +++ b/tests/test_databroker.py @@ -2,7 +2,7 @@ import unittest import os -import testenv +import dotdot import fake_env from pubs import content, filebroker, databroker, datacache @@ -20,17 +20,17 @@ class TestFakeFs(unittest.TestCase): fake_env.unset_fake_fs([content, filebroker]) -class TestDataBroker(TestFakeFs): +class TestDataBroker(unittest.TestCase): def test_databroker(self): ende = endecoder.EnDecoder() page99_metadata = ende.decode_metadata(str_fixtures.metadata_raw0) - page99_bibdata = ende.decode_bibdata(str_fixtures.bibyaml_raw0) + page99_bibdata = ende.decode_bibdata(str_fixtures.bibtex_raw0) for db_class in [databroker.DataBroker, datacache.DataCache]: self.fs = fake_env.create_fake_fs([content, filebroker]) - + db = db_class('tmp', create=True) db.push_metadata('citekey1', page99_metadata) @@ -41,12 +41,17 @@ class TestDataBroker(TestFakeFs): self.assertTrue(db.exists('citekey1', both=True)) self.assertEqual(db.pull_metadata('citekey1'), page99_metadata) + pulled = db.pull_bibdata('citekey1')['Page99'] + for key, value in pulled.items(): + self.assertEqual(pulled[key], page99_bibdata['Page99'][key]) self.assertEqual(db.pull_bibdata('citekey1'), page99_bibdata) + fake_env.unset_fake_fs([content, filebroker]) + def test_existing_data(self): ende = endecoder.EnDecoder() - page99_bibdata = ende.decode_bibdata(str_fixtures.bibyaml_raw0) + page99_bibdata = ende.decode_bibdata(str_fixtures.bibtex_raw0) for db_class in [databroker.DataBroker, datacache.DataCache]: self.fs = fake_env.create_fake_fs([content, filebroker]) @@ -56,8 +61,8 @@ class TestDataBroker(TestFakeFs): self.assertEqual(db.pull_bibdata('Page99'), page99_bibdata) - for citekey in ['10.1371_journal.pone.0038236', - '10.1371journal.pone.0063400', + for citekey in ['10.1371_journal.pone.0038236', + '10.1371journal.pone.0063400', 'journal0063400']: db.pull_bibdata(citekey) db.pull_metadata(citekey) @@ -67,8 +72,14 @@ class TestDataBroker(TestFakeFs): with self.assertRaises(IOError): db.pull_metadata('citekey') - db.copy_doc('Larry99', 'docsdir://Page99.pdf') + db.add_doc('Larry99', 'docsdir://Page99.pdf') self.assertTrue(content.check_file('repo/doc/Page99.pdf', fail=False)) self.assertTrue(content.check_file('repo/doc/Larry99.pdf', fail=False)) db.remove_doc('docsdir://Page99.pdf') + + fake_env.unset_fake_fs([content, filebroker]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_endecoder.py b/tests/test_endecoder.py index 4c6910f..c003563 100644 --- a/tests/test_endecoder.py +++ b/tests/test_endecoder.py @@ -3,10 +3,10 @@ import unittest import yaml -import testenv +import dotdot from pubs import endecoder -from str_fixtures import bibyaml_raw0, bibtexml_raw0, bibtex_raw0, metadata_raw0 +from str_fixtures import bibtex_raw0, metadata_raw0 def compare_yaml_str(s1, s2): if s1 == s2: @@ -19,30 +19,23 @@ def compare_yaml_str(s1, s2): class TestEnDecode(unittest.TestCase): - def test_endecode_bibyaml(self): - - decoder = endecoder.EnDecoder() - entry = decoder.decode_bibdata(bibyaml_raw0) - bibyaml_output0 = decoder.encode_bibdata(entry) - - self.assertEqual(bibyaml_raw0, bibyaml_output0) - self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) - - def test_endecode_bibtexml(self): - - decoder = endecoder.EnDecoder() - entry = decoder.decode_bibdata(bibtexml_raw0) - bibyaml_output0 = decoder.encode_bibdata(entry) - - self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) - def test_endecode_bibtex(self): decoder = endecoder.EnDecoder() entry = decoder.decode_bibdata(bibtex_raw0) - bibyaml_output0 = decoder.encode_bibdata(entry) - self.assertTrue(compare_yaml_str(bibyaml_raw0, bibyaml_output0)) + bibraw1 = decoder.encode_bibdata(entry) + entry1 = decoder.decode_bibdata(bibraw1) + bibraw2 = decoder.encode_bibdata(entry1) + entry2 = decoder.decode_bibdata(bibraw2) + + for citekey in entry1.keys(): + bibentry1 = entry1[citekey] + bibentry2 = entry2[citekey] + for key, value in bibentry1.items(): + self.assertEqual(bibentry1[key], bibentry2[key]) + + self.assertEqual(bibraw1, bibraw2) def test_endecode_metadata(self): @@ -50,5 +43,8 @@ class TestEnDecode(unittest.TestCase): entry = decoder.decode_metadata(metadata_raw0) metadata_output0 = decoder.encode_metadata(entry) - self.assertEqual(metadata_raw0, metadata_output0) + self.assertEqual(set(metadata_raw0.split('\n')), set(metadata_output0.split('\n'))) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_events.py b/tests/test_events.py index c12ec7f..8f09ea3 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,6 +1,6 @@ -from unittest import TestCase +import unittest -import testenv +import dotdot from pubs.events import Event @@ -62,7 +62,7 @@ def test_info_instance(infoevent): _output.append(infoevent.specific) -class TestEvents(TestCase): +class TestEvents(unittest.TestCase): def setUp(self): global _output @@ -88,3 +88,7 @@ class TestEvents(TestCase): SpecificInfo('info', 'specific').send() correct = ['info', 'info', 'specific'] self.assertEquals(_output, correct) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_filebroker.py b/tests/test_filebroker.py index 47c257d..8d8d2cb 100644 --- a/tests/test_filebroker.py +++ b/tests/test_filebroker.py @@ -2,7 +2,7 @@ import unittest import os -import testenv +import dotdot import fake_env from pubs import content, filebroker @@ -35,10 +35,10 @@ class TestFileBroker(TestFakeFs): def test_existing_data(self): - fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'tmpdir'), 'tmpdir') + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'tmpdir'), 'tmpdir') fb = filebroker.FileBroker('tmpdir', create = True) - with open('tmpdir/bib/Page99.bibyaml', 'r') as f: + with open('tmpdir/bib/Page99.bib', 'r') as f: self.assertEqual(fb.pull_bibfile('Page99'), f.read()) with open('tmpdir/meta/Page99.yaml', 'r') as f: @@ -93,12 +93,12 @@ class TestDocBroker(TestFakeFs): def test_doccopy(self): - fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'data'), 'data') + fake_env.copy_dir(self.fs, os.path.join(os.path.dirname(__file__), 'data'), 'data') fb = filebroker.FileBroker('tmpdir', create = True) docb = filebroker.DocBroker('tmpdir') - docpath = docb.copy_doc('Page99', 'data/pagerank.pdf') + docpath = docb.add_doc('Page99', 'data/pagerank.pdf') self.assertTrue(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'))) self.assertTrue(docb.in_docsdir(docpath)) @@ -108,3 +108,7 @@ class TestDocBroker(TestFakeFs): self.assertFalse(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'), fail=False)) with self.assertRaises(IOError): self.assertFalse(content.check_file(os.path.join('tmpdir', 'doc/Page99.pdf'), fail=True)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_paper.py b/tests/test_paper.py index 9f5c3b3..e865fae 100644 --- a/tests/test_paper.py +++ b/tests/test_paper.py @@ -2,7 +2,7 @@ import os import unittest -import testenv +import dotdot import fixtures from pubs.paper import Paper @@ -41,3 +41,7 @@ class TestAttributes(unittest.TestCase): p.remove_tag('ranking') self.assertEqual(p.tags, set()) p.remove_tag('ranking') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_pretty.py b/tests/test_pretty.py new file mode 100644 index 0000000..0437358 --- /dev/null +++ b/tests/test_pretty.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +import unittest +import os + +import dotdot +import fake_env + +from pubs import endecoder, pretty + +from str_fixtures import bibtex_raw0 + +class TestPretty(unittest.TestCase): + + def test_oneliner(self): + + decoder = endecoder.EnDecoder() + bibdata = decoder.decode_bibdata(bibtex_raw0) + pretty.bib_oneliner(bibdata['Page99']) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_queries.py b/tests/test_queries.py index bafa009..0f9147f 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -1,91 +1,101 @@ -from unittest import TestCase +import unittest -import testenv -import fixtures -from papers.commands.list_cmd import (_check_author_match, +import dotdot +from pubs.commands.list_cmd import (_check_author_match, _check_field_match, _check_query_block, filter_paper, InvalidQuery) +from pubs.paper import Paper + +import fixtures + +doe_paper = Paper(fixtures.doe_bibdata) +page_paper = Paper(fixtures.page_bibdata) +turing_paper = Paper(fixtures.turing_bibdata, metadata=fixtures.turing_metadata) -class TestAuthorFilter(TestCase): +class TestAuthorFilter(unittest.TestCase): def test_fails_if_no_author(self): - no_doe = fixtures.doe2013.copy() - no_doe.bibentry.persons = {} + no_doe = doe_paper.deepcopy() + no_doe.bibentry['author'] = [] self.assertTrue(not _check_author_match(no_doe, 'whatever')) def test_match_case(self): - self.assertTrue(_check_author_match(fixtures.doe2013, 'doe')) - self.assertTrue(_check_author_match(fixtures.doe2013, 'doe', + self.assertTrue(_check_author_match(doe_paper, 'doe')) + self.assertTrue(_check_author_match(doe_paper, 'doe', case_sensitive=False)) def test_do_not_match_case(self): - self.assertFalse(_check_author_match(fixtures.doe2013, 'dOe')) - self.assertFalse(_check_author_match(fixtures.doe2013, 'doe', + self.assertFalse(_check_author_match(doe_paper, 'dOe')) + self.assertFalse(_check_author_match(doe_paper, 'doe', case_sensitive=True)) def test_match_not_first_author(self): - self.assertTrue(_check_author_match(fixtures.page99, 'wani')) + self.assertTrue(_check_author_match(page_paper, 'motwani')) def test_do_not_match_first_name(self): - self.assertTrue(not _check_author_match(fixtures.page99, 'larry')) + self.assertTrue(not _check_author_match(page_paper, 'larry')) -class TestCheckTag(TestCase): +class TestCheckTag(unittest.TestCase): pass -class TestCheckField(TestCase): +class TestCheckField(unittest.TestCase): def test_match_case(self): - self.assertTrue(_check_field_match(fixtures.doe2013, 'title', 'nice')) - self.assertTrue(_check_field_match(fixtures.doe2013, 'title', 'nice', + self.assertTrue(_check_field_match(doe_paper, 'title', 'nice')) + self.assertTrue(_check_field_match(doe_paper, 'title', 'nice', case_sensitive=False)) - self.assertTrue(_check_field_match(fixtures.doe2013, 'year', '2013')) + self.assertTrue(_check_field_match(doe_paper, 'year', '2013')) def test_do_not_match_case(self): - self.assertFalse(_check_field_match(fixtures.doe2013, 'title', + self.assertTrue(_check_field_match(doe_paper, 'title', 'Title', case_sensitive=True)) - self.assertFalse(_check_field_match(fixtures.doe2013, 'title', 'nice', + self.assertFalse(_check_field_match(doe_paper, 'title', 'nice', case_sensitive=True)) -class TestCheckQueryBlock(TestCase): +class TestCheckQueryBlock(unittest.TestCase): def test_raise_invalid_if_no_value(self): with self.assertRaises(InvalidQuery): - _check_query_block(fixtures.doe2013, 'title') + _check_query_block(doe_paper, 'title') def test_raise_invalid_if_too_much(self): with self.assertRaises(InvalidQuery): - _check_query_block(fixtures.doe2013, 'whatever:value:too_much') + _check_query_block(doe_paper, 'whatever:value:too_much') -class TestFilterPaper(TestCase): +class TestFilterPaper(unittest.TestCase): def test_case(self): - self.assertTrue (filter_paper(fixtures.doe2013, ['title:nice'])) - self.assertTrue (filter_paper(fixtures.doe2013, ['title:Nice'])) - self.assertFalse(filter_paper(fixtures.doe2013, ['title:nIce'])) + self.assertTrue (filter_paper(doe_paper, ['title:nice'])) + self.assertTrue (filter_paper(doe_paper, ['title:Nice'])) + self.assertFalse(filter_paper(doe_paper, ['title:nIce'])) def test_fields(self): - self.assertTrue (filter_paper(fixtures.doe2013, ['year:2013'])) - self.assertFalse(filter_paper(fixtures.doe2013, ['year:2014'])) - self.assertTrue (filter_paper(fixtures.doe2013, ['author:doe'])) - self.assertTrue (filter_paper(fixtures.doe2013, ['author:Doe'])) + self.assertTrue (filter_paper(doe_paper, ['year:2013'])) + self.assertFalse(filter_paper(doe_paper, ['year:2014'])) + self.assertTrue (filter_paper(doe_paper, ['author:doe'])) + self.assertTrue (filter_paper(doe_paper, ['author:Doe'])) def test_tags(self): - self.assertTrue (filter_paper(fixtures.turing1950, ['tag:computer'])) - self.assertFalse(filter_paper(fixtures.turing1950, ['tag:Ai'])) - self.assertTrue (filter_paper(fixtures.turing1950, ['tag:AI'])) - self.assertTrue (filter_paper(fixtures.turing1950, ['tag:ai'])) + self.assertTrue (filter_paper(turing_paper, ['tag:computer'])) + self.assertFalse(filter_paper(turing_paper, ['tag:Ai'])) + self.assertTrue (filter_paper(turing_paper, ['tag:AI'])) + self.assertTrue (filter_paper(turing_paper, ['tag:ai'])) def test_multiple(self): - self.assertTrue (filter_paper(fixtures.doe2013, + self.assertTrue (filter_paper(doe_paper, ['author:doe', 'year:2013'])) - self.assertFalse(filter_paper(fixtures.doe2013, + self.assertFalse(filter_paper(doe_paper, ['author:doe', 'year:2014'])) - self.assertFalse(filter_paper(fixtures.doe2013, + self.assertFalse(filter_paper(doe_paper, ['author:doee', 'year:2014'])) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_repo.py b/tests/test_repo.py index b57372c..efb4259 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -4,7 +4,7 @@ import shutil import os import fixtures -from pubs.repo import (Repository, _base27, BIB_DIR, META_DIR, +from pubs.repo import (Repository, _base27, CiteKeyCollision) from pubs.paper import PaperInRepo from pubs import configs, files @@ -107,3 +107,76 @@ class TestUpdatePaper(TestRepo): self.repo.doc_dir, 'Turing1950.pdf'))) self.assertTrue(os.path.exists(os.path.join( self.repo.doc_dir, 'Doe2003.pdf'))) + +class TestSaveLoad(unittest.TestCase): + + def setUp(self): + + + self.tmpdir = tempfile.mkdtemp() + os.makedirs(os.path.join(self.tmpdir, 'bibdata')) + os.makedirs(os.path.join(self.tmpdir, 'meta')) + self.bibfile = os.path.join(self.tmpdir, 'bib.bibyaml') + with open(self.bibfile, 'w') as f: + f.write(BIB) + self.metafile = os.path.join(self.tmpdir, 'meta.meta') + with open(self.metafile, 'w') as f: + f.write(META) + self.dest_bibfile = os.path.join(self.tmpdir, 'written_bib.yaml') + self.dest_metafile = os.path.join(self.tmpdir, 'written_meta.yaml') + + def test_load_valid(self): + p = Paper.load(self.bibfile, metapath=self.metafile) + self.assertEqual(fixtures.turing1950, p) + + def test_save_fails_with_no_citekey(self): + p = Paper() + with self.assertRaises(ValueError): + p.save(self.dest_bibfile, self.dest_metafile) + + def test_save_creates_bib(self): + fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) + self.assertTrue(os.path.exists(self.dest_bibfile)) + + def test_save_creates_meta(self): + fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) + self.assertTrue(os.path.exists(self.dest_metafile)) + + def test_save_right_bib(self): + fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) + with open(self.dest_bibfile, 'r') as f: + written = yaml.load(f) + ok = yaml.load(BIB) + self.assertEqual(written, ok) + + def test_save_right_meta(self): + fixtures.turing1950.save(self.dest_bibfile, self.dest_metafile) + with open(self.dest_metafile, 'r') as f: + written = yaml.load(f) + ok = yaml.load(META) + self.assertEqual(written, ok) + + def tearDown(self): + shutil.rmtree(self.tmpdir) + +class TestCopy(unittest.TestCase): + + def setUp(self): + self.orig = Paper() + self.orig.bibentry.fields['title'] = u'Nice title.' + self.orig.bibentry.fields['year'] = u'2013' + self.orig.bibentry.persons['author'] = [Person(u'John Doe')] + self.orig.citekey = self.orig.generate_citekey() + + def test_copy_equal(self): + copy = self.orig.copy() + self.assertEqual(copy, self.orig) + + def test_copy_can_be_changed(self): + copy = self.orig.copy() + copy.bibentry.fields['year'] = 2014 + self.assertEqual(self.orig.bibentry.fields['year'], u'2013') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tag.py b/tests/test_tag.py index f2db3a7..7e08d43 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -1,18 +1,22 @@ # -*- coding: utf-8 -*- import unittest -import testenv +import dotdot from pubs.commands.tag_cmd import _parse_tags, _tag_groups class TestTag(unittest.TestCase): def test_tag_parsing(self): - self.assertEqual(['+abc', '+def9'], _parse_tags( 'abc+def9')) - self.assertEqual(['+abc', '-def9'], _parse_tags( 'abc-def9')) - self.assertEqual(['-abc', '-def9'], _parse_tags('-abc-def9')) - self.assertEqual(['+abc', '-def9'], _parse_tags('+abc-def9')) + self.assertEqual(['+abc', '+def9'], _parse_tags([ 'abc+def9'])) + self.assertEqual(['+abc', '-def9'], _parse_tags([ 'abc-def9'])) + self.assertEqual(['-abc', '-def9'], _parse_tags(['-abc-def9'])) + self.assertEqual(['+abc', '-def9'], _parse_tags(['+abc-def9'])) - self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags('-war+math+romance'))) - self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags('+math+romance-war'))) - self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags('math+romance-war'))) + self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags(['-war+math+romance']))) + self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags(['+math+romance-war']))) + self.assertEqual(({'math', 'romance'}, {'war'}), _tag_groups(_parse_tags(['math+romance-war']))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_usecase.py b/tests/test_usecase.py index 5b10934..4883132 100644 --- a/tests/test_usecase.py +++ b/tests/test_usecase.py @@ -2,7 +2,7 @@ import unittest import re import os -import testenv +import dotdot import fake_env from pubs import pubs_cmd @@ -19,7 +19,6 @@ from pubs.commands import init_cmd, import_cmd class TestFakeInput(unittest.TestCase): def test_input(self): - input = fake_env.FakeInput(['yes', 'no']) self.assertEqual(input(), 'yes') self.assertEqual(input(), 'no') @@ -50,7 +49,7 @@ class CommandTestCase(unittest.TestCase): def setUp(self): self.fs = fake_env.create_fake_fs([content, filebroker, init_cmd, import_cmd]) - def execute_cmds(self, cmds, fs=None): + def execute_cmds(self, cmds, fs=None, capture_output=True): """ Execute a list of commands, and capture their output A command can be a string, or a tuple of size 2 or 3. @@ -67,18 +66,25 @@ class CommandTestCase(unittest.TestCase): input = fake_env.FakeInput(cmd[1], [content, uis, beets_ui, p3]) input.as_global() - _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd[0].split()) - if len(cmd) == 3: - actual_out = color.undye(stdout.getvalue()) - correct_out = color.undye(cmd[2]) - self.assertEqual(actual_out, correct_out) + if capture_output: + _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd[0].split()) + if len(cmd) == 3 and capture_output: + actual_out = color.undye(stdout.getvalue()) + correct_out = color.undye(cmd[2]) + self.assertEqual(actual_out, correct_out) + else: + pubs_cmd.execute(cmd.split()) else: - assert type(cmd) == str - _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd.split()) - - assert(stderr.getvalue() == '') - outs.append(color.undye(stdout.getvalue())) + if capture_output: + assert isinstance(cmd, str) + _, stdout, stderr = fake_env.redirect(pubs_cmd.execute)(cmd.split()) + else: + pubs_cmd.execute(cmd.split()) + + if capture_output: + assert(stderr.getvalue() == '') + outs.append(color.undye(stdout.getvalue())) return outs def tearDown(self): @@ -171,11 +177,11 @@ class TestUsecase(DataCommandTestCase): def test_first(self): correct = ['Initializing pubs in /paper_first.\n', '', - '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n', + '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n', '', '', 'search network\n', - '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) search network\n' + '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) search network\n' ] cmds = ['pubs init -p paper_first/', @@ -236,7 +242,7 @@ class TestUsecase(DataCommandTestCase): bib2 = re.sub('Lawrence Page', 'Lawrence Ridge', bib1) bib3 = re.sub('Page99', 'Ridge07', bib2) - line = '[Page99] L. Page et al. "The PageRank Citation Ranking Bringing Order to the Web" (1999) \n' + line = '[Page99] Page, Lawrence et al. "The PageRank Citation Ranking: Bringing Order to the Web." (1999) \n' line1 = re.sub('1999', '2007', line) line2 = re.sub('L. Page', 'L. Ridge', line1) line3 = re.sub('Page99', 'Ridge07', line2) @@ -258,12 +264,10 @@ class TestUsecase(DataCommandTestCase): cmds = ['pubs init', ('pubs add', [str_fixtures.bibtex_external0]), 'pubs export Page99', - ('pubs export Page99 -f bibtex', []), - 'pubs export Page99 -f bibyaml', ] outs = self.execute_cmds(cmds) - self.assertEqual(endecoder.EnDecoder().decode_bibdata(outs[3]), fixtures.page_bibdata) + self.assertEqual(endecoder.EnDecoder().decode_bibdata(outs[2]), fixtures.page_bibdata) def test_import(self): cmds = ['pubs init', @@ -304,3 +308,7 @@ class TestUsecase(DataCommandTestCase): with self.assertRaises(SystemExit): self.execute_cmds(cmds) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testrepo/bib/10.1371_journal.pone.0038236.bib b/tests/testrepo/bib/10.1371_journal.pone.0038236.bib new file mode 100644 index 0000000..bfd6882 --- /dev/null +++ b/tests/testrepo/bib/10.1371_journal.pone.0038236.bib @@ -0,0 +1,15 @@ + +@article{10.1371_journal.pone.0038236, + author = {Caroline Lyon AND Chrystopher L. Nehaniv AND Joe Saunders}, + journal = {PLoS ONE}, + publisher = {Public Library of Science}, + title = {Interactive Language Learning by Robots: The Transition from Babbling to Word Forms}, + year = {2012}, + month = {06}, + volume = {7}, + url = {http://dx.doi.org/10.1371%2Fjournal.pone.0038236}, + pages = {e38236}, + abstract = {

The advent of humanoid robots has enabled a new approach to investigating the acquisition of language, and we report on the development of robots able to acquire rudimentary linguistic skills. Our work focuses on early stages analogous to some characteristics of a human child of about 6 to 14 months, the transition from babbling to first word forms. We investigate one mechanism among many that may contribute to this process, a key factor being the sensitivity of learners to the statistical distribution of linguistic elements. As well as being necessary for learning word meanings, the acquisition of anchor word forms facilitates the segmentation of an acoustic stream through other mechanisms. In our experiments some salient one-syllable word forms are learnt by a humanoid robot in real-time interactions with naive participants. Words emerge from random syllabic babble through a learning process based on a dialogue between the robot and the human participant, whose speech is perceived by the robot as a stream of phonemes. Numerous ways of representing the speech as syllabic segments are possible. Furthermore, the pronunciation of many words in spontaneous speech is variable. However, in line with research elsewhere, we observe that salient content words are more likely than function words to have consistent canonical representations; thus their relative frequency increases, as does their influence on the learner. Variable pronunciation may contribute to early word form acquisition. The importance of contingent interaction in real-time between teacher and learner is reflected by a reinforcement process, with variable success. The examination of individual cases may be more informative than group results. Nevertheless, word forms are usually produced by the robot after a few minutes of dialogue, employing a simple, real-time, frequency dependent mechanism. This work shows the potential of human-robot interaction systems in studies of the dynamics of early language acquisition.

}, + number = {6}, + doi = {10.1371/journal.pone.0038236} +} diff --git a/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml b/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml deleted file mode 100644 index 26da434..0000000 --- a/tests/testrepo/bib/10.1371_journal.pone.0038236.bibyaml +++ /dev/null @@ -1,45 +0,0 @@ -entries: - 10.1371_journal.pone.0038236: - abstract:

The advent of humanoid robots has enabled a new approach to investigating - the acquisition of language, and we report on the development of robots - able to acquire rudimentary linguistic skills. Our work focuses on early - stages analogous to some characteristics of a human child of about 6 to - 14 months, the transition from babbling to first word forms. We investigate - one mechanism among many that may contribute to this process, a key factor - being the sensitivity of learners to the statistical distribution of linguistic - elements. As well as being necessary for learning word meanings, the acquisition - of anchor word forms facilitates the segmentation of an acoustic stream - through other mechanisms. In our experiments some salient one-syllable - word forms are learnt by a humanoid robot in real-time interactions with - naive participants. Words emerge from random syllabic babble through a - learning process based on a dialogue between the robot and the human participant, - whose speech is perceived by the robot as a stream of phonemes. Numerous - ways of representing the speech as syllabic segments are possible. Furthermore, - the pronunciation of many words in spontaneous speech is variable. However, - in line with research elsewhere, we observe that salient content words - are more likely than function words to have consistent canonical representations; - thus their relative frequency increases, as does their influence on the - learner. Variable pronunciation may contribute to early word form acquisition. - The importance of contingent interaction in real-time between teacher - and learner is reflected by a reinforcement process, with variable success. - The examination of individual cases may be more informative than group - results. Nevertheless, word forms are usually produced by the robot after - a few minutes of dialogue, employing a simple, real-time, frequency dependent - mechanism. This work shows the potential of human-robot interaction systems - in studies of the dynamics of early language acquisition.

- author: - - first: Caroline - last: Saunders - middle: Lyon AND Chrystopher L. Nehaniv AND Joe - doi: 10.1371/journal.pone.0038236 - journal: PLoS ONE - month: '06' - number: '6' - pages: e38236 - publisher: Public Library of Science - title: 'Interactive Language Learning by Robots: The Transition from Babbling - to Word Forms' - type: article - url: http://dx.doi.org/10.1371%2Fjournal.pone.0038236 - volume: '7' - year: '2012' diff --git a/tests/testrepo/bib/10.1371journal.pone.0063400.bib b/tests/testrepo/bib/10.1371journal.pone.0063400.bib new file mode 100644 index 0000000..4bc2500 --- /dev/null +++ b/tests/testrepo/bib/10.1371journal.pone.0063400.bib @@ -0,0 +1,15 @@ + +@article{10.1371/journal.pone.0063400, + author = {Martius, , Georg AND Der, , Ralf AND Ay, , Nihat}, + journal = {PLoS ONE}, + publisher = {Public Library of Science}, + title = {Information Driven Self-Organization of Complex Robotic Behaviors}, + year = {2013}, + month = {05}, + volume = {8}, + url = {http://dx.doi.org/10.1371%2Fjournal.pone.0063400}, + pages = {e63400}, + abstract = {

Information theory is a powerful tool to express principles to drive autonomous systems because it is domain invariant and allows for an intuitive interpretation. This paper studies the use of the predictive information (PI), also called excess entropy or effective measure complexity, of the sensorimotor process as a driving force to generate behavior. We study nonlinear and nonstationary systems and introduce the time-local predicting information (TiPI) which allows us to derive exact results together with explicit update rules for the parameters of the controller in the dynamical systems framework. In this way the information principle, formulated at the level of behavior, is translated to the dynamics of the synapses. We underpin our results with a number of case studies with high-dimensional robotic systems. We show the spontaneous cooperativity in a complex physical system with decentralized control. Moreover, a jointly controlled humanoid robot develops a high behavioral variety depending on its physics and the environment it is dynamically embedded into. The behavior can be decomposed into a succession of low-dimensional modes that increasingly explore the behavior space. This is a promising way to avoid the curse of dimensionality which hinders learning systems to scale well.

}, + number = {5}, + doi = {10.1371/journal.pone.0063400} +} diff --git a/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml b/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml deleted file mode 100644 index bdfda50..0000000 --- a/tests/testrepo/bib/10.1371journal.pone.0063400.bibyaml +++ /dev/null @@ -1,36 +0,0 @@ -entries: - 10.1371journal.pone.0063400: - abstract:

Information theory is a powerful tool to express principles to - drive autonomous systems because it is domain invariant and allows for - an intuitive interpretation. This paper studies the use of the predictive - information (PI), also called excess entropy or effective measure complexity, - of the sensorimotor process as a driving force to generate behavior. We - study nonlinear and nonstationary systems and introduce the time-local - predicting information (TiPI) which allows us to derive exact results - together with explicit update rules for the parameters of the controller - in the dynamical systems framework. In this way the information principle, - formulated at the level of behavior, is translated to the dynamics of - the synapses. We underpin our results with a number of case studies with - high-dimensional robotic systems. We show the spontaneous cooperativity - in a complex physical system with decentralized control. Moreover, a jointly - controlled humanoid robot develops a high behavioral variety depending - on its physics and the environment it is dynamically embedded into. The - behavior can be decomposed into a succession of low-dimensional modes - that increasingly explore the behavior space. This is a promising way - to avoid the curse of dimensionality which hinders learning systems to - scale well.

- author: - - first: Georg - last: Ay - middle: Martius AND Ralf Der AND Nihat - doi: 10.1371/journal.pone.0063400 - journal: PLoS ONE - month: '05' - number: '5' - pages: e63400 - publisher: Public Library of Science - title: Information Driven Self-Organization of Complex Robotic Behaviors - type: article - url: http://dx.doi.org/10.1371%2Fjournal.pone.0063400 - volume: '8' - year: '2013' diff --git a/tests/testrepo/bib/Page99.bib b/tests/testrepo/bib/Page99.bib new file mode 100644 index 0000000..89d2df9 --- /dev/null +++ b/tests/testrepo/bib/Page99.bib @@ -0,0 +1,13 @@ +@techreport{Page99, + number = {1999-66}, + month = {November}, + author = {Lawrence Page and Sergey Brin and Rajeev Motwani and Terry Winograd}, + note = {Previous number = SIDL-WP-1999-0120}, + title = {The PageRank Citation Ranking: Bringing Order to the Web.}, + type = {Technical Report}, + publisher = {Stanford InfoLab}, + year = {1999}, + institution = {Stanford InfoLab}, + url = {http://ilpubs.stanford.edu:8090/422/}, + abstract = {The importance of a Web page is an inherently subjective matter, which depends on the readers interests, knowledge and attitudes. But there is still much that can be said objectively about the relative importance of Web pages. This paper describes PageRank, a mathod for rating Web pages objectively and mechanically, effectively measuring the human interest and attention devoted to them. We compare PageRank to an idealized random Web surfer. We show how to efficiently compute PageRank for large numbers of pages. And, we show how to apply PageRank to search and to user navigation.} +} diff --git a/tests/testrepo/bib/Page99.bibyaml b/tests/testrepo/bib/Page99.bibyaml deleted file mode 100644 index 3e77c1c..0000000 --- a/tests/testrepo/bib/Page99.bibyaml +++ /dev/null @@ -1,28 +0,0 @@ -entries: - Page99: - abstract: The importance of a Web page is an inherently subjective matter, - which depends on the readers interests, knowledge and attitudes. But there - is still much that can be said objectively about the relative importance - of Web pages. This paper describes PageRank, a mathod for rating Web pages - objectively and mechanically, effectively measuring the human interest - and attention devoted to them. We compare PageRank to an idealized random - Web surfer. We show how to efficiently compute PageRank for large numbers - of pages. And, we show how to apply PageRank to search and to user navigation. - author: - - first: Lawrence - last: Page - - first: Sergey - last: Brin - - first: Rajeev - last: Motwani - - first: Terry - last: Winograd - institution: Stanford InfoLab - month: November - note: Previous number = SIDL-WP-1999-0120 - number: 1999-66 - publisher: Stanford InfoLab - title: 'The PageRank Citation Ranking: Bringing Order to the Web.' - type: techreport - url: http://ilpubs.stanford.edu:8090/422/ - year: '1999' diff --git a/tests/testrepo/bib/journal0063400.bib b/tests/testrepo/bib/journal0063400.bib new file mode 100644 index 0000000..292026f --- /dev/null +++ b/tests/testrepo/bib/journal0063400.bib @@ -0,0 +1,6 @@ +@article{10.1371/journal.pone.0063400, + author = {Martius, , Georg AND Der, , Ralf AND Ay, , Nihat}, + journal = {PLoS ONE}, + publisher = {Public Library of Science}, + title = {Information Driven Self-Organization of Complex Robotic Behaviors}, +} diff --git a/tests/testrepo/bib/journal0063400.bibyaml b/tests/testrepo/bib/journal0063400.bibyaml deleted file mode 100644 index 041a029..0000000 --- a/tests/testrepo/bib/journal0063400.bibyaml +++ /dev/null @@ -1,15 +0,0 @@ -entries: - journal0063400: - author: - - first: Lawrence - last: Page - - first: Sergey - last: Brin - - first: Rajeev - last: Motwani - - first: Terry - last: Winograd - journal: PLoS ONE - publisher: Public Library of Science - title: Information Driven Self-Organization of Complex Robotic Behaviors - type: article From 47f54af8fa87815ce166a6d58f114b8dc427a024 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 14 Apr 2014 15:53:33 +0200 Subject: [PATCH 48/48] bump to version 5 --- pubs/__init__.py | 2 +- pubs/configs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubs/__init__.py b/pubs/__init__.py index b350a5d..1941738 100644 --- a/pubs/__init__.py +++ b/pubs/__init__.py @@ -1 +1 @@ -__version__ = 4 +__version__ = 5 diff --git a/pubs/configs.py b/pubs/configs.py index 9d1a446..c6b4313 100644 --- a/pubs/configs.py +++ b/pubs/configs.py @@ -20,7 +20,7 @@ DFT_CONFIG = collections.OrderedDict([ ('import_copy', True), ('import_move', False), ('color', True), - ('version', 4), + ('version', 5), ('version_warning', True), ('open_cmd', 'open'), ('edit_cmd', DFT_EDIT_CMD),