def test_database_read() -> None: """Test the `cobib.database.Database.read` method.""" bib = Database() bib.read() # pylint: disable=protected-access assert Database._unsaved_entries == {} # pylint: disable=C1803 assert list(bib.keys()) == ["einstein", "latexcompanion", "knuthwebsite"]
def test_database_pop() -> None: """Test the `cobib.database.Database.pop` method.""" entries = {"dummy1": "test1", "dummy2": "test2", "dummy3": "test3"} bib = Database() bib.update(entries) # type: ignore # pylint: disable=protected-access Database._unsaved_entries = {} assert Database._unsaved_entries == {} # pylint: disable=C1803 entry = bib.pop("dummy1") assert entry == "test1" assert "dummy1" not in bib.keys() assert Database._unsaved_entries == {"dummy1": None}
def execute(self, args: List[str], out: IO[Any] = sys.stdout) -> None: """Imports new entries from another bibliography manager. The source from which to import new entries is configured via the `args`. The available importers are provided by the `cobib.importers` module. Args: args: a sequence of additional arguments used for the execution. The following values are allowed for this command: * `--skip-download`: skips the automatic download of attached files (like PDFs). * in addition to the options above, a *mutually exclusive group* of keyword arguments for all available `cobib.importers` are registered at runtime. Please check the output of `cobib import --help` for the exact list. * finally, you can add another set of positional arguments (preceded by `--`) which will be passed on to the chosen importer. For more details see for example `cobib import --zotero -- --help`. out: the output IO stream. This defaults to `sys.stdout`. """ LOGGER.debug("Starting Import command.") parser = ArgumentParser(prog="import", description="Import subcommand parser.") parser.add_argument( "--skip-download", action="store_true", help="skip the automatic download of encountered PDF attachments", ) parser.add_argument( "importer_arguments", nargs="*", help="You can pass additional arguments to the chosen importer. To ensure this works as" " expected you should add the pseudo-argument '--' before the remaining arguments.", ) group_import = parser.add_mutually_exclusive_group() avail_parsers = { cls.name: cls for _, cls in inspect.getmembers(importers) if inspect.isclass(cls) } for name in avail_parsers.keys(): try: group_import.add_argument(f"--{name}", action="store_true", help=f"{name} importer") except argparse.ArgumentError: continue if not args: parser.print_usage(sys.stderr) sys.exit(1) try: largs = parser.parse_args(args) except argparse.ArgumentError as exc: LOGGER.error(exc.message) return Event.PreImportCommand.fire(largs) imported_entries: List[Entry] = [] for name, cls in avail_parsers.items(): enabled = getattr(largs, name, False) if not enabled: continue LOGGER.debug("Importing entries from %s.", name) imported_entries = cls().fetch( largs.importer_arguments, skip_download=largs.skip_download ) break bib = Database() existing_labels = set(bib.keys()) new_entries: Dict[str, Entry] = OrderedDict() for entry in imported_entries: # check if label already exists if entry.label in existing_labels: msg = ( f"The label '{entry.label}' already exists. It will be disambiguated based on " "the configuration option: config.database.format.label_suffix" ) LOGGER.warning(msg) new_label = bib.disambiguate_label(entry.label, entry) entry.label = new_label bib.update({entry.label: entry}) existing_labels.add(entry.label) new_entries[entry.label] = entry Event.PostImportCommand.fire(new_entries) bib.update(new_entries) bib.save()
def execute(self, args: List[str], out: IO[Any] = sys.stdout) -> None: """Adds a new entry. Depending on the `args`, if a keyword for one of the available `cobib.parsers` was used together with a matching input, that parser will be used to create the new entry. Otherwise, the command is only valid if the `--label` option was used to specify a new entry label, in which case this command will trigger the `cobib.commands.edit.EditCommand` for a manual entry addition. Args: args: a sequence of additional arguments used for the execution. The following values are allowed for this command: * `-l`, `--label`: the label to give to the new entry. * `-u`, `--update`: updates an existing database entry if it already exists. * `-f`, `--file`: one or multiple files to associate with this entry. This data will be stored in the `cobib.database.Entry.file` property. * `-p`, `--path`: the path to store the downloaded associated file in. This can be used to overwrite the `config.utils.file_downloader.default_location`. * `--skip-download`: skips the automatic download of an associated file. * `--skip-existing`: skips entry if label exists instead of running label disambiguation. * in addition to the options above, a *mutually exclusive group* of keyword arguments for all available `cobib.parsers` are registered at runtime. Please check the output of `cobib add --help` for the exact list. * any *positional* arguments (i.e. those, not preceded by a keyword) are interpreted as tags and will be stored in the `cobib.database.Entry.tags` property. out: the output IO stream. This defaults to `sys.stdout`. """ LOGGER.debug("Starting Add command.") parser = ArgumentParser(prog="add", description="Add subcommand parser.") parser.add_argument("-l", "--label", type=str, help="the label for the new database entry") parser.add_argument( "-u", "--update", action="store_true", help="update an entry if the label exists already", ) file_action = "extend" if sys.version_info[1] >= 8 else "append" parser.add_argument( "-f", "--file", type=str, nargs="+", action=file_action, help="files associated with this entry", ) parser.add_argument("-p", "--path", type=str, help="the path for the associated file") parser.add_argument( "--skip-download", action="store_true", help="skip the automatic download of an associated file", ) parser.add_argument( "--skip-existing", action="store_true", help="skips entry addition if existent instead of using label disambiguation", ) group_add = parser.add_mutually_exclusive_group() avail_parsers = { cls.name: cls for _, cls in inspect.getmembers(parsers) if inspect.isclass(cls) } for name in avail_parsers.keys(): try: group_add.add_argument( f"-{name[0]}", f"--{name}", type=str, help=f"{name} object identfier" ) except argparse.ArgumentError: try: group_add.add_argument(f"--{name}", type=str, help=f"{name} object identfier") except argparse.ArgumentError: continue parser.add_argument( "tags", nargs=argparse.REMAINDER, help="A list of space-separated tags to associate with this entry." "\nYou can use quotes to specify tags with spaces in them.", ) if not args: parser.print_usage(sys.stderr) sys.exit(1) try: largs = parser.parse_args(args) except argparse.ArgumentError as exc: LOGGER.error(exc.message) return Event.PreAddCommand.fire(largs) new_entries: Dict[str, Entry] = OrderedDict() edit_entries = False for name, cls in avail_parsers.items(): string = getattr(largs, name, None) if string is None: continue LOGGER.debug("Adding entries from %s: '%s'.", name, string) new_entries = cls().parse(string) break else: if largs.label is not None: LOGGER.warning("No input to parse. Creating new entry '%s' manually.", largs.label) new_entries = { largs.label: Entry( largs.label, {"ENTRYTYPE": config.commands.edit.default_entry_type}, ) } edit_entries = True else: msg = "Neither an input to parse nor a label for manual creation specified!" LOGGER.error(msg) return if largs.label is not None: assert len(new_entries.values()) == 1 for value in new_entries.values(): # logging done by cobib/database/entry.py value.label = largs.label new_entries = OrderedDict((largs.label, value) for value in new_entries.values()) else: formatted_entries = OrderedDict() for label, value in new_entries.items(): formatted_label = evaluate_as_f_string( config.database.format.label_default, {"label": label, **value.data.copy()} ) value.label = formatted_label formatted_entries[formatted_label] = value new_entries = formatted_entries if largs.file is not None: if file_action == "append": # We need to flatten the potentially nested list. # pylint: disable=import-outside-toplevel from itertools import chain largs.file = list(chain.from_iterable(largs.file)) assert len(new_entries.values()) == 1 for value in new_entries.values(): # logging done by cobib/database/entry.py value.file = largs.file if largs.tags != []: assert len(new_entries.values()) == 1 for value in new_entries.values(): # logging done by cobib/database/entry.py value.tags = largs.tags bib = Database() existing_labels = set(bib.keys()) for lbl, entry in new_entries.copy().items(): # check if label already exists if lbl in existing_labels: if not largs.update: msg = f"You tried to add a new entry '{lbl}' which already exists!" LOGGER.warning(msg) if edit_entries or largs.skip_existing: msg = f"Please use `cobib edit {lbl}` instead!" LOGGER.warning(msg) continue msg = ( "The label will be disambiguated based on the configuration option: " "config.database.format.label_suffix" ) LOGGER.warning(msg) new_label = bib.disambiguate_label(lbl, entry) entry.label = new_label new_entries[new_label] = entry new_entries.pop(lbl) else: # label exists but the user asked to update an existing entry existing_data = bib[lbl].data.copy() existing_data.update(entry.data) entry.data = existing_data.copy() # download associated file (if requested) if "_download" in entry.data.keys(): if largs.skip_download: entry.data.pop("_download") else: path = FileDownloader().download( entry.data.pop("_download"), lbl, folder=largs.path, overwrite=largs.update ) if path is not None: entry.data["file"] = str(path) # check journal abbreviation if "journal" in entry.data.keys(): entry.data["journal"] = JournalAbbreviations.elongate(entry.data["journal"]) Event.PostAddCommand.fire(new_entries) bib.update(new_entries) if edit_entries: EditCommand().execute([largs.label]) bib.save() self.git(args=vars(largs)) for label in new_entries: msg = f"'{label}' was added to the database." LOGGER.info(msg)