From d6828811f350824abd074cf33389b33c72b4262e Mon Sep 17 00:00:00 2001 From: RoDuth Date: Thu, 10 Oct 2024 07:19:45 +1000 Subject: [PATCH] Type annotations corrections Now type annotations are more commonly added than not fix mypy issues. Some of this is scrappy and would be better dealt with by upgrading to sqlalchemy 2 etc.. OsmGpsMap [attr-defined] import error only when run on the file itself not the whole repo? prefs.testing = True not needed in most situtations and hence removed. Removed a couple of monkey patches in tests, use mock instead Removed unused monkey patching of models to db. Some context: https://github.com/python/typing/discussions/1102 https://github.com/sqlalchemy/sqlalchemy/discussions/9321 https://github.com/python/mypy/issues/16426 --- .pylintrc | 4 +- bauble/__init__.py | 2 +- bauble/btypes.py | 2 +- bauble/db.py | 6 +- bauble/editor.py | 21 +++--- bauble/i18n.py | 27 +++---- bauble/paths.py | 4 +- bauble/pluginmgr.py | 28 +++----- bauble/plugins/abcd/__init__.py | 34 +++++---- bauble/plugins/garden/__init__.py | 9 --- bauble/plugins/garden/accession.py | 53 ++++++-------- bauble/plugins/garden/garden_map.py | 4 +- bauble/plugins/garden/location.py | 28 +++++--- bauble/plugins/garden/plant.py | 46 +++++++----- bauble/plugins/garden/propagation.py | 15 ++-- bauble/plugins/garden/source.py | 17 +++-- bauble/plugins/garden/test_garden.py | 5 +- bauble/plugins/imex/__init__.py | 6 +- bauble/plugins/imex/csv_.py | 16 +++-- bauble/plugins/imex/shapefile/export_tool.py | 10 +-- bauble/plugins/imex/shapefile/import_tool.py | 2 +- .../plugins/imex/shapefile/test_shapefile.py | 2 +- bauble/plugins/imex/test_csv_io.py | 7 +- bauble/plugins/plants/family.py | 31 ++++---- bauble/plugins/plants/genus.py | 35 ++++++---- bauble/plugins/plants/species.py | 5 +- bauble/plugins/plants/species_editor.py | 9 ++- bauble/plugins/plants/species_model.py | 70 +++++++++++-------- bauble/plugins/plants/test_plants.py | 2 +- bauble/plugins/plants/test_stored_queries.py | 2 - bauble/plugins/report/__init__.py | 5 +- bauble/plugins/report/mako/__init__.py | 2 +- bauble/plugins/synclone/sync.py | 26 ++++--- bauble/plugins/tag/__init__.py | 2 +- bauble/plugins/tag/test_tag.py | 2 +- bauble/query_builder.py | 16 ++--- bauble/search/clauses.py | 18 ++--- bauble/test/test_connmgr.py | 3 - bauble/test/test_db.py | 4 -- bauble/test/test_prefs.py | 20 ++---- bauble/test/test_search.py | 3 - bauble/utils/__init__.py | 12 ++-- bauble/utils/desktop.py | 55 +++++---------- bauble/utils/geo.py | 2 +- bauble/view.py | 10 ++- requirements-dev.txt | 20 ++++++ 46 files changed, 363 insertions(+), 339 deletions(-) create mode 100644 requirements-dev.txt diff --git a/.pylintrc b/.pylintrc index 9104582e1..3d81bf3f4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -610,5 +610,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/bauble/__init__.py b/bauble/__init__.py index acdac366d..39d1cea11 100755 --- a/bauble/__init__.py +++ b/bauble/__init__.py @@ -39,7 +39,7 @@ from bauble import paths from bauble.version import version -version_tuple = tuple(version.split(".")) +version_tuple: tuple[str, ...] = tuple(version.split(".")) release_date = datetime.datetime.fromtimestamp(0, datetime.UTC) release_version = None installation_date = datetime.datetime.now() diff --git a/bauble/btypes.py b/bauble/btypes.py index 09ff5feae..9da73b7af 100755 --- a/bauble/btypes.py +++ b/bauble/btypes.py @@ -198,7 +198,7 @@ class CustomEnum(Enum): cache_ok = False - def __init__(self, size: str, **kwargs: Any) -> None: + def __init__(self, size: int, **kwargs: Any) -> None: """Pass the size parameter to impl column (Unicode). To complete initialisation call `self.init` when values become diff --git a/bauble/db.py b/bauble/db.py index 5ba3f8d03..afd8773bb 100644 --- a/bauble/db.py +++ b/bauble/db.py @@ -94,13 +94,13 @@ def get_active_children(children: Callable | str, obj: Any) -> Iterable: """Return only active children of obj if the 'exclude_inactive' pref is set True else return all children. """ - children = children(obj) if callable(children) else getattr(obj, children) + kids = children(obj) if callable(children) else getattr(obj, children) # avoid circular refs from bauble import prefs if prefs.prefs.get(prefs.exclude_inactive_pref): - return [i for i in children if getattr(i, "active", True)] - return children + return [i for i in kids if getattr(i, "active", True)] + return kids class MapperBase(DeclarativeMeta): diff --git a/bauble/editor.py b/bauble/editor.py index 3f7f788ba..72316d50d 100755 --- a/bauble/editor.py +++ b/bauble/editor.py @@ -20,12 +20,12 @@ Description: a collection of functions and abstract classes for creating editors """ - import datetime import json import logging import os import re +import threading import weakref from collections.abc import Callable from pathlib import Path @@ -194,7 +194,8 @@ class GenericEditorView: parent=None then bauble.gui.window is used """ - _tooltips = {} + _tooltips: dict[str, str] = {} + accept_buttons: list[str] = [] def __init__( self, filename, parent=None, root_widget_name=None, tooltips=None @@ -380,7 +381,9 @@ def connect_signals(self, target): def set_accept_buttons_sensitive(self, sensitive): """set the sensitivity of all the accept/ok buttons""" - for wname in self.accept_buttons: # pylint: disable=no-member + if not self.accept_buttons: + raise AttributeError("accept_buttons not set.") + for wname in self.accept_buttons: getattr(self.widgets, wname).set_sensitive(sensitive) def connect(self, obj, signal, callback, *args): @@ -1136,8 +1139,8 @@ class GenericEditorPresenter: view should trigger a session.commit. """ - widget_to_field_map = {} - view_accept_buttons = [] + widget_to_field_map: dict[str, str] = {} + view_accept_buttons: list[str] = [] PROBLEM_DUPLICATE = f"duplicate:{random()}" PROBLEM_EMPTY = f"empty:{random()}" @@ -1154,11 +1157,11 @@ def __init__( ): self.model = model self.view = view - self.problems = set() + self.problems: set[tuple[int, Gtk.Widget]] = set() self._dirty = False self.is_committing_presenter = do_commit self.committing_results = committing_results - self.running_threads = [] + self.running_threads: list[threading.Thread] = [] self.owns_session = False self.session = session if session is False: @@ -1997,7 +2000,7 @@ def on_map_delete(self, *_args): def on_map_kml_show(self, *_args): import tempfile - from mako.template import Template + from mako.template import Template # type: ignore [import-untyped] template = Template( filename=self.kml_template, @@ -2103,7 +2106,7 @@ class GenericModelViewPresenterEditor: :param parent: the parent windows for the view or None """ - ok_responses = () + ok_responses: tuple[int, ...] = () def __init__(self, model, parent=None): self.session = db.Session() diff --git a/bauble/i18n.py b/bauble/i18n.py index 28ff0eae0..0ab8192e7 100755 --- a/bauble/i18n.py +++ b/bauble/i18n.py @@ -3,7 +3,7 @@ # Copyright (c) 2007 Kopfgeldjaeger # Copyright (c) 2012-2017 Mario Frasca # Copyright 2017 Jardín Botánico de Quito -# Copyright (c) 2022 Ross Demuth +# Copyright (c) 2022-2024 Ross Demuth # # This file is part of ghini.desktop. # @@ -50,7 +50,7 @@ __all__ = ["_"] -TEXT_DOMAIN = "ghini-%s" % ".".join(version_tuple[0:2]) +TEXT_DOMAIN = f"ghini-{'.'.join(version_tuple[0:2])}" # most of the following code was adapted from: # http://www.learningpython.com/2006/12/03/\ @@ -58,11 +58,7 @@ langs = [] # Check the default locale -try: - # Python >= 3.11 - lang_code, encoding = locale.getlocale() -except AttributeError: - lang_code, encoding = locale.getdefaultlocale() +lang_code, encoding = locale.getlocale() if lang_code: # If we have a default, it's the first in the list @@ -81,17 +77,14 @@ # use. First we check the default, then what the system told us, and # finally the 'known' list -if sys.platform in ["win32", "darwin"]: - locale = gettext +# NOTE not sure about this, commenting for now +# if sys.platform in ["win32", "darwin"]: +# locale = gettext +# locale.bindtextdomain(TEXT_DOMAIN, paths.locale_dir()) +# locale.textdomain(TEXT_DOMAIN) -try: - import Gtk.glade as gtkglade -except ImportError: - gtkglade = locale - -for module in locale, gtkglade: - module.bindtextdomain(TEXT_DOMAIN, paths.locale_dir()) - module.textdomain(TEXT_DOMAIN) +gettext.bindtextdomain(TEXT_DOMAIN, paths.locale_dir()) +gettext.textdomain(TEXT_DOMAIN) # Get the language to use lang = gettext.translation( diff --git a/bauble/paths.py b/bauble/paths.py index 936d012a0..aee5dbd11 100755 --- a/bauble/paths.py +++ b/bauble/paths.py @@ -118,7 +118,9 @@ def appdata_dir(): ) elif sys.platform == "darwin": # pylint: disable=no-name-in-module - from AppKit import NSApplicationSupportDirectory + from AppKit import ( # type: ignore [import-untyped] # noqa + NSApplicationSupportDirectory, + ) from AppKit import NSSearchPathForDirectoriesInDomains from AppKit import NSUserDomainMask diff --git a/bauble/pluginmgr.py b/bauble/pluginmgr.py index 92e636766..3af785863 100755 --- a/bauble/pluginmgr.py +++ b/bauble/pluginmgr.py @@ -60,9 +60,9 @@ from bauble.error import BaubleError from bauble.i18n import _ -plugins = {} -commands = {} -provided = {} +plugins: dict[str, "Plugin"] = {} +commands: dict[str, type["CommandHandler"]] = {} +provided: dict[str, type[db.Base]] = {} def register_command(handler): @@ -456,10 +456,10 @@ class Plugin: a map of commands this plugin handled with callbacks, e.g dict('cmd', lambda x: handler) tools: - a list of BaubleTool classes that this plugin provides, the + a list of Tool classes that this plugin provides, the tools' category and label will be used in Ghini's "Tool" menu depends: - a list of names classes that inherit from BaublePlugin that this + a list of class names that inherit from Plugin that this plugin depends on provides: a dictionary name->class exported by this plugin @@ -467,10 +467,10 @@ class Plugin: a short description of the plugin """ - commands = [] - tools = [] - depends = [] - provides = {} + commands: list[type["CommandHandler"]] = [] + tools: list[type["Tool"]] = [] + depends: list[str] = [] + provides: dict[str, type] = {} description = "" version = "0.0" @@ -491,14 +491,6 @@ def install(cls, import_defaults=True): pass -class EditorPlugin(Plugin): - """a plugin that provides one or more editors, the editors should - implement the Editor interface - """ - - editors = [] - - class Tool: # pylint: disable=too-few-public-methods category: str | None = None label: str @@ -564,7 +556,7 @@ def show_all(self) -> None: class CommandHandler(ABC): - command: str | Iterable[str] + command: str | Iterable[str | None] def get_view(self) -> View | None: """return the view for this command handler""" diff --git a/bauble/plugins/abcd/__init__.py b/bauble/plugins/abcd/__init__.py index 55bcccef0..143183ace 100755 --- a/bauble/plugins/abcd/__init__.py +++ b/bauble/plugins/abcd/__init__.py @@ -37,8 +37,10 @@ from gi.repository import Gtk from lxml import etree from lxml.etree import Element +from lxml.etree import ElementBase from lxml.etree import ElementTree from lxml.etree import SubElement +from lxml.etree import _ElementTree from sqlalchemy.orm import object_session from bauble import db @@ -190,6 +192,14 @@ def extra_elements(self, unit): def species_markup(self, unit): """The species markup""" + @abstractmethod + def get_datelastedited(self): + """Get the date last edited""" + + @abstractmethod + def get_notes(self, unit): + """Get the associated notes""" + class SpeciesABCDAdapter(ABCDAdapter): """An adapter to convert a Species to an ABCD Unit. @@ -321,14 +331,14 @@ def extra_elements(self, unit): # invalid XML file if self.for_reports: if self.species.label_distribution: - etree.SubElement( - unit, "LabelDistribution" - ).text = self.species.label_distribution + etree.SubElement(unit, "LabelDistribution").text = ( + self.species.label_distribution + ) if self.species.distribution: - etree.SubElement( - unit, "Distribution" - ).text = self.species.distribution_str() + etree.SubElement(unit, "Distribution").text = ( + self.species.distribution_str() + ) def species_markup(self, unit): if self.for_reports: @@ -495,9 +505,9 @@ def extra_elements(self, unit): def species_markup(self, unit): if self.for_reports: # first the non marked up version - etree.SubElement( - unit, "FullSpeciesName" - ).text = self.accession.species_str() + etree.SubElement(unit, "FullSpeciesName").text = ( + self.accession.species_str() + ) unit.append( etree.fromstring( @@ -563,10 +573,10 @@ def __init__( self.decorated_objects = decorated_objects self.authors = authors self.datasets = data_sets() - self.units = None + self.units: ElementBase | None = None self.inst = institution.Institution() - def _create_units_element(self) -> SubElement: + def _create_units_element(self) -> ElementBase: """Create the base of the 'Units' subelement""" if not verify_institution(self.inst): msg = _( @@ -734,7 +744,7 @@ def generate_elements(self) -> Generator[None, None, None]: ) yield - def get_element_tree(self) -> ElementTree: + def get_element_tree(self) -> _ElementTree: """Call after `generate_elements` has been called.""" return ElementTree(self.datasets) diff --git a/bauble/plugins/garden/__init__.py b/bauble/plugins/garden/__init__.py index a0a6416ae..4f92af1c1 100755 --- a/bauble/plugins/garden/__init__.py +++ b/bauble/plugins/garden/__init__.py @@ -581,12 +581,3 @@ def on_combo_changed(combo, *_args): plugin = GardenPlugin - -# make names visible to db module -db.Accession = Accession -db.AccessionNote = AccessionNote -db.Plant = Plant -db.PlantNote = PlantNote -db.PlantPicture = PlantPicture -db.Location = Location -db.LocationNote = LocationNote diff --git a/bauble/plugins/garden/accession.py b/bauble/plugins/garden/accession.py index 0e95f004f..1a0849246 100755 --- a/bauble/plugins/garden/accession.py +++ b/bauble/plugins/garden/accession.py @@ -51,6 +51,7 @@ from sqlalchemy import literal from sqlalchemy.exc import DBAPIError from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Session from sqlalchemy.orm import backref from sqlalchemy.orm import object_mapper from sqlalchemy.orm import relationship @@ -84,6 +85,7 @@ from ..plants.species_editor import species_cell_data_func from ..plants.species_editor import species_match_func from ..plants.species_editor import species_to_string_matcher +from .location import Location from .propagation import Propagation from .propagation import SourcePropagationPresenter from .source import COLLECTION_KML_MAP_PREF @@ -354,7 +356,9 @@ class Verification(db.Base): # pylint: disable=too-few-public-methods reference = Column(UnicodeText) accession_id = Column(Integer, ForeignKey("accession.id"), nullable=False) - accession = relationship("Accession", back_populates="verifications") + accession: "Accession" = relationship( + "Accession", back_populates="verifications" + ) # the level of assurance of this verification level = Column(Integer, nullable=False, autoincrement=False) @@ -365,10 +369,10 @@ class Verification(db.Base): # pylint: disable=too-few-public-methods # what it was verified from prev_species_id = Column(Integer, ForeignKey("species.id"), nullable=False) - species = relationship( + species: "Species" = relationship( "Species", primaryjoin="Verification.species_id==Species.id" ) - prev_species = relationship( + prev_species: "Species" = relationship( "Species", primaryjoin="Verification.prev_species_id==Species.id" ) @@ -395,7 +399,7 @@ class Voucher(db.Base): # pylint: disable=too-few-public-methods code = Column(Unicode(32), nullable=False) parent_material = Column(types.Boolean, default=False) accession_id = Column(Integer, ForeignKey("accession.id"), nullable=False) - accession = relationship( + accession: "Accession" = relationship( "Accession", uselist=False, back_populates="vouchers" ) @@ -406,9 +410,9 @@ class IntendedLocation(db.Base): # pylint: disable=too-few-public-methods date = Column(types.Date, default=func.now()) planted = Column(types.Boolean, default=False) location_id = Column(Integer, ForeignKey("location.id"), nullable=False) - location = relationship("Location") + location: Location = relationship("Location") accession_id = Column(Integer, ForeignKey("accession.id"), nullable=False) - accession = relationship("Accession") + accession: "Accession" = relationship("Accession") # ITF2 - E.1; Provenance Type Flag; Transfer code: prot @@ -603,6 +607,7 @@ class Accession(db.Base, db.WithNotes): __tablename__ = "accession" + id: int # columns #: the accession code code = Column(Unicode(20), nullable=False, unique=True) @@ -661,20 +666,20 @@ class Accession(db.Base, db.WithNotes): supplied_name = Column(Unicode) # use backref not back_populates here to avoid InvalidRequestError in # view.multiproc_counter - species = relationship( + species: "Species" = relationship( "Species", uselist=False, backref=backref("accessions", cascade="all, delete-orphan"), ) # intended location - intended_locations = relationship( + intended_locations: list["IntendedLocation"] = relationship( "IntendedLocation", cascade="all, delete-orphan", back_populates="accession", ) - source = relationship( + source: "Source" = relationship( "Source", uselist=False, cascade="all, delete-orphan", @@ -682,17 +687,17 @@ class Accession(db.Base, db.WithNotes): ) # use Plant.code for the order_by to avoid ambiguous column names - plants = relationship( + plants: list["Plant"] = relationship( "Plant", cascade="all, delete-orphan", back_populates="accession" ) - verifications = relationship( + verifications: list["Verification"] = relationship( "Verification", order_by="Verification.date", cascade="all, delete-orphan", ) - vouchers = relationship( + vouchers: list["Voucher"] = relationship( "Voucher", cascade="all, delete-orphan", back_populates="accession" ) @@ -804,7 +809,7 @@ def propagations(self) -> list: @property def pictures(self) -> list[Picture]: session = object_session(self) - if not session: + if not isinstance(session, Session): return [] # avoid circular imports from .plant import PlantPicture @@ -815,7 +820,7 @@ def pictures(self) -> list[Picture]: .filter(Accession.id == self.id) ) if prefs.prefs.get(prefs.exclude_inactive_pref): - plt_pics = plt_pics.filter(Plant.active.is_(True)) + plt_pics = plt_pics.filter(Plant.active.is_(True)) # type: ignore [attr-defined] # noqa return plt_pics.all() @hybrid_property @@ -830,7 +835,7 @@ def active(self): return True return False - @active.expression + @active.expression # type: ignore [no-redef] def active(cls): # pylint: disable=no-self-argument inactive = ( @@ -950,11 +955,6 @@ class AccessionEditorView(editor.GenericEditorView): also provides some utility methods for changing widget states. """ - expanders_pref_map = { - # 'acc_notes_expander': 'editor.accession.notes.expanded', - # 'acc_source_expander': 'editor.accession.source.expanded' - } - _tooltips = { "acc_species_entry": _( "The species must be selected from the list of completions. " @@ -1111,17 +1111,6 @@ def set_accept_buttons_sensitive(self, sensitive): self.widgets.acc_ok_and_add_button.set_sensitive(sensitive) self.widgets.acc_next_button.set_sensitive(sensitive) - def save_state(self): - """save the current state of the gui to the preferences""" - for expander, pref in self.expanders_pref_map.items(): - prefs.prefs[pref] = self.widgets[expander].get_expanded() - - def restore_state(self): - """restore the state of the gui from the preferences""" - for expander, pref in self.expanders_pref_map.items(): - expanded = prefs.prefs.get(pref, True) - self.widgets[expander].set_expanded(expanded) - # staticmethod ensures the AccessionEditorView gets garbage collected. @staticmethod def datum_match_func(completion, key, treeiter): @@ -1304,7 +1293,7 @@ def refresh(self, intended): def on_map_kml_show(self, *_args): import tempfile - from mako.template import Template + from mako.template import Template # type: ignore [import-untyped] from .location import LOC_KML_MAP_PREFS diff --git a/bauble/plugins/garden/garden_map.py b/bauble/plugins/garden/garden_map.py index 6c46b2bc7..d86226d12 100644 --- a/bauble/plugins/garden/garden_map.py +++ b/bauble/plugins/garden/garden_map.py @@ -44,7 +44,7 @@ from gi.repository import Gio from gi.repository import GLib from gi.repository import Gtk -from gi.repository import OsmGpsMap # type: ignore[attr-defined] +from gi.repository import OsmGpsMap # NOTE mypy [attr-defined] only on file from sqlalchemy import Table from sqlalchemy import case from sqlalchemy import engine @@ -822,7 +822,7 @@ def __init__(self, garden_map: GardenMap) -> None: self.context_menu: Gtk.Menu self.init_context_menu() self.zoom_to_home() - self._resize_timer_id = None + self._resize_timer_id: int | None = None @staticmethod def is_visible() -> bool: diff --git a/bauble/plugins/garden/location.py b/bauble/plugins/garden/location.py index 1863e26be..53e35d95b 100755 --- a/bauble/plugins/garden/location.py +++ b/bauble/plugins/garden/location.py @@ -23,6 +23,7 @@ import os import traceback from pathlib import Path +from typing import TYPE_CHECKING logger = logging.getLogger(__name__) @@ -32,6 +33,7 @@ from sqlalchemy import UnicodeText from sqlalchemy import literal from sqlalchemy.exc import DBAPIError +from sqlalchemy.orm import Session from sqlalchemy.orm import backref from sqlalchemy.orm import deferred from sqlalchemy.orm import relationship @@ -56,6 +58,10 @@ from bauble.view import Action from bauble.view import Picture +if TYPE_CHECKING: + from .accession import IntendedLocation + from .plant import Plant + def edit_callback(locations): e = LocationEditor(model=locations[0]) @@ -172,6 +178,8 @@ class Location(db.Base, db.WithNotes): """ + id: int + __tablename__ = "location" # columns @@ -191,8 +199,10 @@ class Location(db.Base, db.WithNotes): geojson = deferred(Column(types.JSON())) # relations - plants = relationship("Plant", backref=backref("location", uselist=False)) - intended_accessions = relationship( + plants: list["Plant"] = relationship( + "Plant", backref=backref("location", uselist=False) + ) + intended_accessions: list["IntendedLocation"] = relationship( "IntendedLocation", cascade="all, delete-orphan", back_populates="location", @@ -204,7 +214,7 @@ class Location(db.Base, db.WithNotes): def pictures(self) -> list[Picture]: """Return pictures from any attached plants and any in _pictures.""" session = object_session(self) - if not session: + if not isinstance(session, Session): return [] # avoid circular imports from ..garden import Plant @@ -216,7 +226,7 @@ def pictures(self) -> list[Picture]: .filter(Location.id == self.id) ) if prefs.prefs.get(prefs.exclude_inactive_pref): - plt_pics = plt_pics.filter(Plant.active.is_(True)) + plt_pics = plt_pics.filter(Plant.active.is_(True)) # type: ignore [attr-defined] # noqa return plt_pics.all() + self._pictures @classmethod @@ -475,14 +485,14 @@ def handle_response(self, response): more_committed = None if response == self.RESPONSE_NEXT: self.presenter.cleanup() - e = LocationEditor(parent=self.parent) - more_committed = e.start() + editor = LocationEditor(parent=self.parent) + more_committed = editor.start() elif response == self.RESPONSE_OK_AND_ADD: from bauble.plugins.garden.plant import Plant from bauble.plugins.garden.plant import PlantEditor - e = PlantEditor(Plant(location=self.model), self.parent) - more_committed = e.start() + editor = PlantEditor(Plant(location=self.model), self.parent) + more_committed = editor.start() if more_committed is not None: if isinstance(more_committed, list): self._committed.extend(more_committed) @@ -492,7 +502,7 @@ def handle_response(self, response): return True def start(self): - """Start the LocationEditor and return the committed Location objects.""" + """Start the LocationEditor and return the committed objects.""" while True: response = self.presenter.start() self.presenter.view.save_state() diff --git a/bauble/plugins/garden/plant.py b/bauble/plugins/garden/plant.py index 83c8cd3ff..ef08e73a2 100755 --- a/bauble/plugins/garden/plant.py +++ b/bauble/plugins/garden/plant.py @@ -405,8 +405,8 @@ def search(self, text: str, session: Session) -> list[Query]: return [] val = vals[0] + acc_code = plant_code = val if operator != "in": - acc_code = plant_code = val if delimiter in val: acc_code, plant_code = val.rsplit(delimiter, 1) @@ -507,8 +507,10 @@ def search(self, text: str, session: Session) -> list[Query]: .join(Accession) .filter( exists().where( - Accession.code == sql_vals.c.acc_code, - Plant.code == sql_vals.c.plt_code, + and_( + Accession.code == sql_vals.c.acc_code, + Plant.code == sql_vals.c.plt_code, + ) ) ) ) @@ -629,20 +631,20 @@ class PlantChange(db.Base): date = Column(types.DateTime(timezone=True), default=func.now()) # relations - plant = relationship( + plant: "Plant" = relationship( "Plant", uselist=False, primaryjoin="PlantChange.plant_id == Plant.id", backref=backref("changes", cascade="all, delete-orphan"), ) - parent_plant = relationship( + parent_plant: "Plant" = relationship( "Plant", uselist=False, primaryjoin="PlantChange.parent_plant_id == Plant.id", backref=backref("branches"), ) - child_plant = relationship( + child_plant: "Plant" = relationship( "Plant", uselist=False, primaryjoin="PlantChange.child_plant_id == Plant.id", @@ -651,10 +653,10 @@ class PlantChange(db.Base): ), ) - from_location = relationship( + from_location: "Location" = relationship( "Location", primaryjoin="PlantChange.from_location_id == Location.id" ) - to_location = relationship( + to_location: "Location" = relationship( "Location", primaryjoin="PlantChange.to_location_id == Location.id" ) @@ -791,7 +793,7 @@ class Plant(db.Base, db.WithNotes): """ __tablename__ = "plant" - __table_args__ = (UniqueConstraint("code", "accession_id"), {}) + __table_args__: tuple = (UniqueConstraint("code", "accession_id"), {}) # columns code = Column(Unicode(6), nullable=False) @@ -806,11 +808,11 @@ class Plant(db.Base, db.WithNotes): quantity = Column(Integer, autoincrement=False, nullable=False) accession_id = Column(Integer, ForeignKey("accession.id"), nullable=False) - accession = relationship( + accession: "Accession" = relationship( "Accession", lazy="subquery", uselist=False, back_populates="plants" ) - location_id = Column(Integer, ForeignKey(Location.id), nullable=False) + location_id = Column(Integer, ForeignKey("location.id"), nullable=False) # spatial data deferred mainly to avoid comparison issues in union search # (i.e. reports) NOTE that deferring can lead to the instance becoming # dirty when merged into another session (i.e. an editor) and the column @@ -827,7 +829,7 @@ class Plant(db.Base, db.WithNotes): "propagation", creator=lambda prop: PlantPropagation(propagation=prop), ) - _plant_props = relationship( + _plant_props: list["PlantPropagation"] = relationship( "PlantPropagation", cascade="all, delete-orphan", uselist=True, @@ -836,7 +838,7 @@ class Plant(db.Base, db.WithNotes): # provide a way to search and use the change that recorded either a death # or a planting date directly. This is not fool proof but close enough. - death = relationship( + death: "PlantChange" = relationship( "PlantChange", primaryjoin="and_(PlantChange.plant_id == Plant.id, " "PlantChange.id == select([PlantChange.id])" @@ -853,7 +855,7 @@ class Plant(db.Base, db.WithNotes): uselist=False, ) - planted = relationship( + planted: "PlantChange" = relationship( "PlantChange", primaryjoin="and_(" "PlantChange.plant_id == Plant.id, " @@ -957,7 +959,7 @@ def delimiter(self): def active(self): return self.quantity > 0 - @active.expression + @active.expression # type: ignore [no-redef] def active(cls): # pylint: disable=no-self-argument from sqlalchemy.sql.expression import case @@ -1330,7 +1332,10 @@ def acc_match_func( :return: bool, True if the item at the treeiter matches the key """ - accession = completion.get_model()[treeiter][0] + tree_model = completion.get_model() + if not tree_model: + raise AttributeError(f"can't get TreeModel from {completion}") + accession = tree_model[treeiter][0] return acc_to_string_matcher(accession, key) @@ -2132,10 +2137,10 @@ def handle_response(self, response): more_committed = None if response == self.RESPONSE_NEXT: self.presenter.cleanup() - e = PlantEditor( + editor = PlantEditor( Plant(accession=self.model.accession), parent=self.parent ) - more_committed = e.start() + more_committed = editor.start() if more_committed is not None: self._committed = [self._committed] @@ -2549,5 +2554,8 @@ def plant_match_func( :return: bool, True if the item at the treeiter matches the key """ - plant = completion.get_model()[treeiter][0] + tree_model = completion.get_model() + if not tree_model: + raise AttributeError(f"can't get TreeModel from {completion}") + plant = tree_model[treeiter][0] return plant_to_string_matcher(plant, key) diff --git a/bauble/plugins/garden/propagation.py b/bauble/plugins/garden/propagation.py index 0c1de8167..4516d3dae 100644 --- a/bauble/plugins/garden/propagation.py +++ b/bauble/plugins/garden/propagation.py @@ -25,6 +25,7 @@ import os import traceback import weakref +from typing import TYPE_CHECKING logger = logging.getLogger(__name__) @@ -35,6 +36,7 @@ from sqlalchemy import UnicodeText from sqlalchemy.exc import DBAPIError from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import Mapped from sqlalchemy.orm import backref from sqlalchemy.orm import relationship from sqlalchemy.orm.session import object_session @@ -47,6 +49,9 @@ from bauble import utils from bauble.i18n import _ +if TYPE_CHECKING: + from . import Plant + prop_type_values = { "Seed": _("Seed"), "UnrootedCutting": _("Unrooted cutting"), @@ -70,6 +75,8 @@ class PlantPropagation(db.Base): propagation_id = Column( Integer, ForeignKey("propagation.id"), nullable=False ) + plant: Mapped["Plant"] + propagation: Mapped["Propagation"] class Propagation(db.Base): @@ -90,21 +97,21 @@ class Propagation(db.Base): "plant", creator=lambda plant: PlantPropagation(plant=plant), ) - _plant_prop = relationship( + _plant_prop: "PlantPropagation" = relationship( "PlantPropagation", cascade="all, delete-orphan", uselist=False, backref=backref("propagation", uselist=False), ) - cutting = relationship( + cutting: "PropCutting" = relationship( "PropCutting", primaryjoin="Propagation.id==PropCutting.propagation_id", cascade="all,delete-orphan", uselist=False, backref=backref("propagation", uselist=False), ) - seed = relationship( + seed: "PropSeed" = relationship( "PropSeed", primaryjoin="Propagation.id==PropSeed.propagation_id", cascade="all,delete-orphan", @@ -407,7 +414,7 @@ class PropCutting(db.Base): Integer, ForeignKey("propagation.id"), nullable=False ) - rooted = relationship( + rooted: "PropCuttingRooted" = relationship( "PropCuttingRooted", cascade="all, delete-orphan", primaryjoin="PropCutting.id == PropCuttingRooted.cutting_id", diff --git a/bauble/plugins/garden/source.py b/bauble/plugins/garden/source.py index 4d228b18c..a0332f973 100755 --- a/bauble/plugins/garden/source.py +++ b/bauble/plugins/garden/source.py @@ -28,6 +28,7 @@ import weakref from pathlib import Path from random import random +from typing import TYPE_CHECKING logger = logging.getLogger(__name__) @@ -60,6 +61,10 @@ from ..plants.geography import Geography from ..plants.geography import GeographyMenu +if TYPE_CHECKING: + from .accession import Accession + from .propagation import Propagation + def collection_edit_callback(coll): from .accession import edit_callback @@ -162,18 +167,18 @@ class Source(db.Base): accession_id = Column( Integer, ForeignKey("accession.id"), nullable=False, unique=True ) - accession = relationship( + accession: "Accession" = relationship( "Accession", uselist=False, back_populates="source" ) source_detail_id = Column(Integer, ForeignKey("source_detail.id")) - source_detail = relationship( + source_detail: "SourceDetail" = relationship( "SourceDetail", uselist=False, backref=backref("sources", cascade="all, delete-orphan"), ) - collection = relationship( + collection: "Collection" = relationship( "Collection", uselist=False, cascade="all, delete-orphan", @@ -184,7 +189,7 @@ class Source(db.Base): # not attached to a Plant # i.e. a Propagation of source material (i.e. purchased seeds etc.) propagation_id = Column(Integer, ForeignKey("propagation.id")) - propagation = relationship( + propagation: "Propagation" = relationship( "Propagation", uselist=False, single_parent=True, @@ -197,7 +202,7 @@ class Source(db.Base): # to a Plant # i.e. a plant is propagation from to create a new accession plant_propagation_id = Column(Integer, ForeignKey("propagation.id")) - plant_propagation = relationship( + plant_propagation: "Propagation" = relationship( "Propagation", uselist=False, primaryjoin="Source.plant_propagation_id==Propagation.id", @@ -316,7 +321,7 @@ class Collection(db.Base): ) source_id = Column(Integer, ForeignKey("source.id"), unique=True) - source = relationship("Source", back_populates="collection") + source: "Source" = relationship("Source", back_populates="collection") retrieve_cols = [ "id", diff --git a/bauble/plugins/garden/test_garden.py b/bauble/plugins/garden/test_garden.py index 619c7881b..7ccac4226 100644 --- a/bauble/plugins/garden/test_garden.py +++ b/bauble/plugins/garden/test_garden.py @@ -103,9 +103,6 @@ from .source import SourceDetail from .source import SourceDetailPresenter -prefs.testing = True - - accession_test_data = ( { "id": 1, @@ -394,7 +391,7 @@ def setUp_data(): inst.write() -setUp_data.order = 1 +setUp_data.order = 1 # type: ignore [attr-defined] # TODO: if we ever get a GUI tester then do the following diff --git a/bauble/plugins/imex/__init__.py b/bauble/plugins/imex/__init__.py index 7e0840e46..3055d60d3 100755 --- a/bauble/plugins/imex/__init__.py +++ b/bauble/plugins/imex/__init__.py @@ -55,7 +55,7 @@ # missing columns so that all columns will have some value -def is_importable_attr(domain: db.Base, path: str) -> bool: +def is_importable_attr(domain: type[db.Base], path: str) -> bool: """Check if a path points to an importable attribute (i.e. can be set). For hybrid_property returns False if has no setter. @@ -84,7 +84,7 @@ class GenericImporter(ABC): # pylint: disable=too-many-instance-attributes Impliment `_import_task` """ - OPTIONS_MAP = [] + OPTIONS_MAP: list[dict] = [] def __init__(self): self.option = "0" @@ -320,7 +320,7 @@ def organise_record(rec: dict) -> dict: for k in sorted(rec, key=lambda i: i.count("."), reverse=True): # get rid of empty strings record[k] = None if rec[k] == "" else rec[k] - organised = {} + organised: dict = {} for k, v in record.items(): if "." in k: path, atr = k.rsplit(".", 1) diff --git a/bauble/plugins/imex/csv_.py b/bauble/plugins/imex/csv_.py index 42405a421..e080b62bb 100755 --- a/bauble/plugins/imex/csv_.py +++ b/bauble/plugins/imex/csv_.py @@ -31,6 +31,7 @@ import tempfile import traceback from pathlib import Path +from typing import Generator logger = logging.getLogger(__name__) @@ -648,7 +649,7 @@ def changes_upgrader(filenames): pb_set_fraction(count / num_lines) yield - def geo_upgrader(self, change_lst: list[str]) -> None: + def geo_upgrader(self, change_lst: list[str]) -> Generator: """Upgrade changes from v1.0 to v1.3 prior to importing""" for file in change_lst: original = file + ORIG_SUFFIX @@ -674,6 +675,8 @@ def geo_upgrader(self, change_lst: list[str]) -> None: ): in_file = csv.DictReader(old) fieldnames = in_file.fieldnames + if not fieldnames: + raise AttributeError(f"can't get fieldnames from {old}") out_file = csv.DictWriter(new, fieldnames=fieldnames) out_file.writeheader() for count, line in enumerate(in_file): @@ -688,8 +691,8 @@ def geo_upgrader(self, change_lst: list[str]) -> None: pb_set_fraction(count / num_lines) yield - def set_geo_translator(self, geography: str) -> None: - """return a dictionary of old IDs to new IDs for the geography table.""" + def set_geo_translator(self, geography: str) -> Generator: + """return a dictionary of old to new IDs for the geography table.""" from bauble.plugins.plants import Geography msg = _("creating translation table") @@ -705,7 +708,7 @@ def set_geo_translator(self, geography: str) -> None: geo = csv.DictReader(f) for count, line in enumerate(geo): id_ = line.get("id") - code = line.get("tdwg_code").split(",")[0] + code = line.get("tdwg_code", "").split(",")[0] parent = line.get("parent_id") old_geos[id_] = {"code": code, "parent": parent} # update the gui @@ -714,6 +717,9 @@ def set_geo_translator(self, geography: str) -> None: pb_set_fraction(fraction) yield + if db.Session is None: + raise ValueError("db.Session is None") + session = db.Session() translator = {} # make sure to get the right count @@ -735,7 +741,7 @@ def set_geo_translator(self, geography: str) -> None: .all() ) if not new: - parent2 = old_geos.get(parent.get("parent")) + parent2 = old_geos.get(codes.get("parent")) if not parent2: logger.debug("no parent for %s", codes) continue diff --git a/bauble/plugins/imex/shapefile/export_tool.py b/bauble/plugins/imex/shapefile/export_tool.py index 0e2dceea2..705dbc0f4 100644 --- a/bauble/plugins/imex/shapefile/export_tool.py +++ b/bauble/plugins/imex/shapefile/export_tool.py @@ -29,7 +29,7 @@ from gi.repository import Gdk from gi.repository import GLib from gi.repository import Gtk -from shapefile import Writer +from shapefile import Writer # type: ignore [import-untyped] from sqlalchemy.ext.associationproxy import AssociationProxy from sqlalchemy.orm import class_mapper @@ -44,16 +44,16 @@ # NOTE importing shapefile Writer above wipes out gettext _ from bauble.i18n import _ from bauble.meta import get_default +from bauble.plugins.garden.location import Location from bauble.plugins.garden.location import ( # noqa pylint: disable=unused-import - Location, + LocationNote, ) -from bauble.plugins.garden.location import LocationNote # NOTE: need to import the Note classes as we may need them. +from bauble.plugins.garden.plant import Plant from bauble.plugins.garden.plant import ( # noqa pylint: disable=unused-import - Plant, + PlantNote, ) -from bauble.plugins.garden.plant import PlantNote from bauble.utils.geo import ProjDB from .. import GenericExporter diff --git a/bauble/plugins/imex/shapefile/import_tool.py b/bauble/plugins/imex/shapefile/import_tool.py index d2f810552..3c3ef327c 100644 --- a/bauble/plugins/imex/shapefile/import_tool.py +++ b/bauble/plugins/imex/shapefile/import_tool.py @@ -32,7 +32,7 @@ from dateutil import parser from gi.repository import Gtk -from shapefile import Reader +from shapefile import Reader # type: ignore [import-untyped] from shapefile import ShapeRecord from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import InspectionAttr diff --git a/bauble/plugins/imex/shapefile/test_shapefile.py b/bauble/plugins/imex/shapefile/test_shapefile.py index 579850066..f4c3a7153 100644 --- a/bauble/plugins/imex/shapefile/test_shapefile.py +++ b/bauble/plugins/imex/shapefile/test_shapefile.py @@ -29,7 +29,7 @@ from zipfile import ZipFile from gi.repository import Gtk -from shapefile import Reader +from shapefile import Reader # type: ignore [import-untyped] from shapefile import Writer from sqlalchemy.ext.hybrid import hybrid_property diff --git a/bauble/plugins/imex/test_csv_io.py b/bauble/plugins/imex/test_csv_io.py index a524af9dd..feb6381a7 100644 --- a/bauble/plugins/imex/test_csv_io.py +++ b/bauble/plugins/imex/test_csv_io.py @@ -494,10 +494,9 @@ def test_relation_filter(self): class CSVExportToolTests(BaubleTestCase): @mock.patch("bauble.plugins.imex.csv_io.message_dialog") - @mock.patch("bauble.gui", **{"get_view.return_value": None}) - def test_no_search_view_asks_to_search_first( - self, _mock_get_view, mock_dialog - ): + @mock.patch("bauble.gui") + def test_no_search_view_asks_to_search_first(self, mock_gui, mock_dialog): + mock_gui.get_view.return_value = None tool = CSVExportTool() result = tool.start() self.assertIsNone(result) diff --git a/bauble/plugins/plants/family.py b/bauble/plugins/plants/family.py index f64a18a2d..a696ed913 100755 --- a/bauble/plugins/plants/family.py +++ b/bauble/plugins/plants/family.py @@ -38,6 +38,8 @@ from sqlalchemy import literal from sqlalchemy.exc import DBAPIError from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import Session from sqlalchemy.orm import relationship from sqlalchemy.orm import synonym as sa_synonym from sqlalchemy.orm import validates @@ -165,8 +167,10 @@ class Family(db.Base, db.WithNotes): The family table has a unique constraint on family/qualifier. """ + id: int + __tablename__ = "family" - __table_args__ = (UniqueConstraint("family", "author"), {}) + __table_args__: tuple = (UniqueConstraint("family", "author"), {}) rank = "familia" link_keys = ["accepted"] @@ -176,7 +180,7 @@ class Family(db.Base, db.WithNotes): suborder = Column(Unicode(64)) family = Column(String(45), nullable=False, index=True) - epithet = sa_synonym("family") + epithet: "str" = sa_synonym("family") # use '' instead of None so that the constraints will work propertly author = Column(Unicode(128), default="") @@ -191,7 +195,7 @@ class Family(db.Base, db.WithNotes): synonyms = association_proxy( "_synonyms", "synonym", creator=lambda fam: FamilySynonym(synonym=fam) ) - _synonyms = relationship( + _synonyms: list["FamilySynonym"] = relationship( "FamilySynonym", primaryjoin="Family.id==FamilySynonym.family_id", cascade="all, delete-orphan", @@ -199,7 +203,7 @@ class Family(db.Base, db.WithNotes): backref="family", ) - _accepted = relationship( + _accepted: "FamilySynonym" = relationship( "FamilySynonym", primaryjoin="Family.id==FamilySynonym.synonym_id", cascade="all, delete-orphan", @@ -210,7 +214,7 @@ class Family(db.Base, db.WithNotes): "_accepted", "family", creator=lambda fam: FamilySynonym(family=fam) ) - genera = relationship( + genera: list["Genus"] = relationship( "Genus", order_by="Genus.genus", back_populates="family", @@ -224,7 +228,7 @@ class Family(db.Base, db.WithNotes): @property def pictures(self) -> list[Picture]: session = object_session(self) - if not session: + if not isinstance(session, Session): return [] from ..garden import Accession from ..garden import Plant @@ -242,7 +246,7 @@ def pictures(self) -> list[Picture]: .filter(Family.id == self.id) ) if prefs.prefs.get(prefs.exclude_inactive_pref): - plt_pics = plt_pics.filter(Plant.active.is_(True)) + plt_pics = plt_pics.filter(Plant.active.is_(True)) # type: ignore [attr-defined] # noqa return sp_pics.all() + plt_pics.all() @classmethod @@ -345,6 +349,8 @@ class FamilySynonym(db.Base): Integer, ForeignKey("family.id"), nullable=False, unique=True ) is_one_to_one = True + synonym: Mapped["Family"] + family: Mapped["Family"] def __str__(self): return Family.str(self.synonym) @@ -604,11 +610,11 @@ def handle_response(self, response): more_committed = None if response == self.RESPONSE_NEXT: self.presenter.cleanup() - e = FamilyEditor(parent=self.parent) - more_committed = e.start() + editor = FamilyEditor(parent=self.parent) + more_committed = editor.start() elif response == self.RESPONSE_OK_AND_ADD: - e = GenusEditor(Genus(family=self.model), self.parent) - more_committed = e.start() + editor = GenusEditor(Genus(family=self.model), self.parent) + more_committed = editor.start() if more_committed is not None: if isinstance(more_committed, list): @@ -874,6 +880,3 @@ def update(self, row): self.synonyms.update(row) self.links.update(row) self.props.update(row) - - -db.Family = Family diff --git a/bauble/plugins/plants/genus.py b/bauble/plugins/plants/genus.py index e3aabfcec..b659882c7 100755 --- a/bauble/plugins/plants/genus.py +++ b/bauble/plugins/plants/genus.py @@ -42,6 +42,7 @@ from sqlalchemy.exc import DBAPIError from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Mapped from sqlalchemy.orm import Session from sqlalchemy.orm import backref from sqlalchemy.orm import object_mapper @@ -184,8 +185,10 @@ class Genus(db.Base, db.WithNotes): and family_id must be unique. """ + id: int + __tablename__ = "genus" - __table_args__ = ( + __table_args__: tuple = ( UniqueConstraint("genus", "author", "qualifier", "family_id"), {}, ) @@ -200,8 +203,8 @@ class Genus(db.Base, db.WithNotes): tribe = Column(Unicode(64)) subtribe = Column(Unicode(64)) - genus = Column(String(64), nullable=False, index=True) - epithet = sa_synonym("genus") + genus: "str" = Column(String(64), nullable=False, index=True) + epithet: "str" = sa_synonym("genus") # use '' instead of None so that the constraints will work propertly author = Column(Unicode(128), default="") @@ -217,7 +220,7 @@ class Genus(db.Base, db.WithNotes): synonyms = association_proxy( "_synonyms", "synonym", creator=lambda gen: GenusSynonym(synonym=gen) ) - _synonyms = relationship( + _synonyms: list["GenusSynonym"] = relationship( "GenusSynonym", primaryjoin="Genus.id==GenusSynonym.genus_id", cascade="all, delete-orphan", @@ -228,7 +231,7 @@ class Genus(db.Base, db.WithNotes): # this is a dummy relation, it is only here to make cascading work # correctly and to ensure that all synonyms related to this genus # get deleted if this genus gets deleted - _accepted = relationship( + _accepted: "GenusSynonym" = relationship( "GenusSynonym", primaryjoin="Genus.id==GenusSynonym.synonym_id", cascade="all, delete-orphan", @@ -239,13 +242,13 @@ class Genus(db.Base, db.WithNotes): "_accepted", "genus", creator=lambda gen: GenusSynonym(genus=gen) ) - species = relationship( + species: list["Species"] = relationship( "Species", cascade="all, delete-orphan", order_by="Species.sp", backref=backref("genus", lazy="subquery", uselist=False), ) - family = relationship("Family", back_populates="genera") + family: "Family" = relationship("Family", back_populates="genera") _cites = Column(types.Enum(values=["I", "II", "III", None]), default=None) @@ -295,7 +298,7 @@ def search_view_markup_pair(self): @property def pictures(self) -> list[Picture]: session = object_session(self) - if not session: + if not isinstance(session, Session): return [] # avoid circular imports from ..garden import Accession @@ -314,7 +317,7 @@ def pictures(self) -> list[Picture]: .filter(Genus.id == self.id) ) if prefs.prefs.get(prefs.exclude_inactive_pref): - plt_pics = plt_pics.filter(Plant.active.is_(True)) + plt_pics = plt_pics.filter(Plant.active.is_(True)) # type: ignore [attr-defined] # noqa return sp_pics.all() + plt_pics.all() @hybrid_property @@ -325,7 +328,7 @@ def cites(self): """ return self._cites or self.family.cites - @cites.expression + @cites.expression # type: ignore [no-redef] def cites(cls): # pylint: disable=no-self-argument,protected-access # subquery required to get the joins in @@ -336,7 +339,7 @@ def cites(cls): ) return case((cls._cites.is_not(None), cls._cites), else_=fam_cites) - @cites.setter + @cites.setter # type: ignore [no-redef] def cites(self, value): self._cites = value @@ -455,6 +458,8 @@ class GenusSynonym(db.Base): Integer, ForeignKey("genus.id"), nullable=False, unique=True ) is_one_to_one = True + synonym: Mapped["Genus"] + genus: Mapped["Genus"] def __str__(self): return f"{str(self.synonym)} ({self.synonym.family})" @@ -519,7 +524,10 @@ def genus_match_func( :return: bool, True if the item at the treeiter matches the key """ - genus = completion.get_model()[treeiter][0] + tree_model = completion.get_model() + if not tree_model: + raise AttributeError(f"can't get TreeModel from {completion}") + genus = tree_model[treeiter][0] if not sa_inspect(genus).persistent: return False return genus_to_string_matcher(genus, key, gen_path) @@ -1206,6 +1214,3 @@ def update(self, row): self.synonyms.update(row) self.links.update(row) self.props.update(row) - - -db.Genus = Genus diff --git a/bauble/plugins/plants/species.py b/bauble/plugins/plants/species.py index f17745518..c49c2452e 100755 --- a/bauble/plugins/plants/species.py +++ b/bauble/plugins/plants/species.py @@ -332,6 +332,7 @@ def get_ids( ids: dict[tuple[type[db.Base], type[db.Base]], set[int]] = {} for result in results: models: tuple[type[db.Base], type[db.Base]] | None = None + id_ = None if isinstance(result, Species): models = (Species, SpeciesSynonym) id_ = result.id @@ -344,7 +345,7 @@ def get_ids( elif isinstance(result, VernacularName): models = (VernacularName, SpeciesSynonym) id_ = result.species.id # type: ignore[attr-defined] - if models: + if models and id_: ids.setdefault(models, set()).add(id_) return ids @@ -377,7 +378,7 @@ def search(self, text: str, session: Session) -> list[Query]: query = ( session.query(models[0]) .join(Species) - .join(SpeciesSynonym, syn_model_id == Species.id) # type: ignore[attr-defined] # noqa + .join(SpeciesSynonym, syn_model_id == Species.id) .filter(syn_id.in_(id_set)) ) else: diff --git a/bauble/plugins/plants/species_editor.py b/bauble/plugins/plants/species_editor.py index 36a49bc8b..d579fa146 100755 --- a/bauble/plugins/plants/species_editor.py +++ b/bauble/plugins/plants/species_editor.py @@ -154,7 +154,10 @@ def species_match_func( :return: bool, True if the item at the treeiter matches the key """ - species = completion.get_model()[treeiter][0] + tree_model = completion.get_model() + if not tree_model: + raise AttributeError(f"can't get TreeModel from {completion}") + species = tree_model[treeiter][0] if not sa_inspect(species).persistent: return False return species_to_string_matcher(species, key, sp_path) @@ -1526,8 +1529,8 @@ def append_dists_from_text(self, text: str) -> None: """ geo_names = [i.strip() for i in text.strip().split(",")] - levels_counter = {} - name_map = {} + levels_counter: dict[int, int] = {} + name_map: dict[str, list[Geography]] = {} unresolved = set() code_re = re.compile(r"^[0-9A-Z-]{1,6}$") diff --git a/bauble/plugins/plants/species_model.py b/bauble/plugins/plants/species_model.py index 2df09e49a..d37eed5e7 100755 --- a/bauble/plugins/plants/species_model.py +++ b/bauble/plugins/plants/species_model.py @@ -38,6 +38,8 @@ from sqlalchemy import literal from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import Session from sqlalchemy.orm import backref from sqlalchemy.orm import object_session from sqlalchemy.orm import relationship @@ -57,6 +59,7 @@ from bauble.view import Picture from .geography import DistributionMap +from .geography import Geography def _remove_zws(string): @@ -322,8 +325,13 @@ class Species(db.Base, db.WithNotes): *distribution*: """ + id: int + __tablename__ = "species" - __table_args__ = (UniqueConstraint("full_sci_name", name="sp_name"), {}) + __table_args__: tuple = ( + UniqueConstraint("full_sci_name", name="sp_name"), + {}, + ) # for internal use when importing records, accounts for the lack of # UniqueConstraint and the complex of hybrid_properties etc. @@ -361,7 +369,7 @@ class Species(db.Base, db.WithNotes): subseries = Column(Unicode(64)) sp = Column(Unicode(128), index=True) - epithet = sa_synonym("sp") + epithet: "str" = sa_synonym("sp") sp_author = Column(Unicode(128)) hybrid = Column(types.Enum(values=["×", "+", None]), default=None) sp_qual = Column( @@ -421,7 +429,7 @@ class Species(db.Base, db.WithNotes): synonyms = association_proxy( "_synonyms", "synonym", creator=lambda sp: SpeciesSynonym(synonym=sp) ) - _synonyms = relationship( + _synonyms: list["SpeciesSynonym"] = relationship( "SpeciesSynonym", primaryjoin="Species.id==SpeciesSynonym.species_id", cascade="all, delete-orphan", @@ -430,7 +438,7 @@ class Species(db.Base, db.WithNotes): ) # make cascading work - _accepted = relationship( + _accepted: "SpeciesSynonym" = relationship( "SpeciesSynonym", primaryjoin="Species.id==SpeciesSynonym.synonym_id", cascade="all, delete-orphan", @@ -442,29 +450,31 @@ class Species(db.Base, db.WithNotes): ) # VernacularName.species gets defined here too. - vernacular_names = relationship( + vernacular_names: list["VernacularName"] = relationship( "VernacularName", cascade="all, delete-orphan", collection_class=VNList, backref=backref("species", uselist=False), ) - _default_vernacular_name = relationship( + _default_vernacular_name: "DefaultVernacularName" = relationship( "DefaultVernacularName", uselist=False, cascade="all, delete-orphan", backref=backref("species", uselist=False), ) - distribution = relationship( + distribution: list["SpeciesDistribution"] = relationship( "SpeciesDistribution", cascade="all, delete-orphan", backref=backref("species", uselist=False), ) habit_id = Column(Integer, ForeignKey("habit.id"), default=None) - habit = relationship("Habit", uselist=False, backref="species") + habit: "Habit" = relationship("Habit", uselist=False, backref="species") flower_color_id = Column(Integer, ForeignKey("color.id"), default=None) - flower_color = relationship("Color", uselist=False, backref="species") + flower_color: "Color" = relationship( + "Color", uselist=False, backref="species" + ) full_name = Column(Unicode(512), index=True) full_sci_name = Column(Unicode(512), index=True) @@ -554,7 +564,7 @@ def cites(self): """ return self._cites or self.genus.cites - @cites.expression + @cites.expression # type: ignore [no-redef] def cites(cls): # pylint: disable=no-self-argument,protected-access from .family import Family @@ -578,7 +588,7 @@ def cites(cls): else_=fam_cites, ) - @cites.setter + @cites.setter # type: ignore [no-redef] def cites(self, value): self._cites = value @@ -615,7 +625,7 @@ def __lowest_infraspecific(self): def infraspecific_rank(self): return self.__lowest_infraspecific()[0] or "" - @infraspecific_rank.expression + @infraspecific_rank.expression # type: ignore [no-redef] def infraspecific_rank(cls): # pylint: disable=no-self-argument # use the last epithet that is not 'cv'. available (the user should be @@ -633,7 +643,7 @@ def infraspecific_rank(cls): def infraspecific_epithet(self): return self.__lowest_infraspecific()[1] or "" - @infraspecific_epithet.expression + @infraspecific_epithet.expression # type: ignore [no-redef] def infraspecific_epithet(cls): # pylint: disable=no-self-argument # use the last epithet that is not 'cv'. @@ -665,7 +675,7 @@ def infraspecific_parts(self): parts = " ".join(parts) return parts - @infraspecific_parts.expression + @infraspecific_parts.expression # type: ignore [no-redef] def infraspecific_parts(cls): # pylint: disable=no-self-argument from sqlalchemy.types import String @@ -732,7 +742,7 @@ def infraspecific_parts(cls): ] ).label("infraspecific_parts") - @infraspecific_parts.setter + @infraspecific_parts.setter # type: ignore [no-redef] def infraspecific_parts(self, value): if value: parts = value.split() @@ -752,7 +762,7 @@ def default_vernacular_name(self): return None return self._default_vernacular_name.vernacular_name - @default_vernacular_name.expression + @default_vernacular_name.expression # type: ignore [no-redef] def default_vernacular_name(cls): # pylint: disable=no-self-argument return ( @@ -767,7 +777,7 @@ def default_vernacular_name(cls): .label("default_vernacular_name") ) - @default_vernacular_name.setter + @default_vernacular_name.setter # type: ignore [no-redef] def default_vernacular_name(self, vernacular): if isinstance(vernacular, str): logger.debug("vernacular_name is a string: %s", vernacular) @@ -796,7 +806,7 @@ def default_vernacular_name(self, vernacular): default_vernacular.vernacular_name = vernacular self._default_vernacular_name = default_vernacular - @default_vernacular_name.deleter + @default_vernacular_name.deleter # type: ignore [no-redef] def default_vernacular_name(self): if self._default_vernacular_name: utils.delete_or_expunge(self._default_vernacular_name) @@ -806,7 +816,7 @@ def default_vernacular_name(self): def family_name(self): return self.genus.family.epithet - @family_name.expression + @family_name.expression # type: ignore [no-redef] def family_name(cls): # pylint: disable=no-self-argument from .family import Family @@ -1025,7 +1035,7 @@ def active(self): return True return False - @active.expression + @active.expression # type: ignore [no-redef] def active(cls): # pylint: disable=no-self-argument acc_cls = cls.accessions.prop.mapper.class_ @@ -1043,7 +1053,7 @@ def active(cls): def pictures(self) -> list[Picture]: """Return pictures from any attached plants and any in _pictures.""" session = object_session(self) - if not session: + if not isinstance(session, Session): return [] # avoid circular imports from ..garden import Accession @@ -1056,7 +1066,7 @@ def pictures(self) -> list[Picture]: .filter(Species.id == self.id) ) if prefs.prefs.get(prefs.exclude_inactive_pref): - plt_pics = plt_pics.filter(Plant.active.is_(True)) + plt_pics = plt_pics.filter(Plant.active.is_(True)) # type: ignore [attr-defined] # noqa return plt_pics.all() + self._pictures infrasp_attr = { @@ -1218,6 +1228,8 @@ class SpeciesSynonym(db.Base): Integer, ForeignKey("species.id"), nullable=False, unique=True ) is_one_to_one = True + species: Mapped["Species"] + synonym: Mapped["Species"] def __str__(self): return str(self.synonym) @@ -1247,7 +1259,7 @@ class VernacularName(db.Base): name = Column(Unicode(128), nullable=False) language = Column(Unicode(128)) species_id = Column(Integer, ForeignKey("species.id"), nullable=False) - __table_args__ = ( + __table_args__: tuple = ( UniqueConstraint("name", "language", "species_id", name="vn_index"), {}, ) @@ -1332,7 +1344,7 @@ class DefaultVernacularName(db.Base): """ __tablename__ = "default_vernacular_name" - __table_args__ = ( + __table_args__: tuple = ( UniqueConstraint( "species_id", "vernacular_name_id", name="default_vn_index" ), @@ -1369,7 +1381,9 @@ class SpeciesDistribution(db.Base): species: Species species_id = Column(Integer, ForeignKey("species.id"), nullable=False) geography_id = Column(Integer, ForeignKey("geography.id"), nullable=False) - geography = relationship("Geography", back_populates="distribution") + geography: Geography = relationship( + "Geography", back_populates="distribution" + ) def __str__(self): return str(self.geography) @@ -1397,9 +1411,3 @@ def __str__(self): if self.name: return f"{self.name} ({self.code})" return str(self.code) - - -db.Species = Species -db.SpeciesNote = SpeciesNote -db.SpeciesPicture = SpeciesPicture -db.VernacularName = VernacularName diff --git a/bauble/plugins/plants/test_plants.py b/bauble/plugins/plants/test_plants.py index d49453d48..b1b334bc1 100644 --- a/bauble/plugins/plants/test_plants.py +++ b/bauble/plugins/plants/test_plants.py @@ -662,7 +662,7 @@ def setUp_data(): utils.reset_sequence(col) -setUp_data.order = 0 +setUp_data.order = 0 # type: ignore [attr-defined] def setup_geographies() -> None: diff --git a/bauble/plugins/plants/test_stored_queries.py b/bauble/plugins/plants/test_stored_queries.py index f19d9c045..5b7237702 100644 --- a/bauble/plugins/plants/test_stored_queries.py +++ b/bauble/plugins/plants/test_stored_queries.py @@ -21,8 +21,6 @@ from bauble.plugins.plants.stored_queries import StoredQueriesPresenter from bauble.test import BaubleTestCase -bauble.prefs.testing = True - class StoredQueriesInitializeTests(BaubleTestCase): def test_initialize_model(self): diff --git a/bauble/plugins/report/__init__.py b/bauble/plugins/report/__init__.py index 8a14387de..0bd694c9e 100755 --- a/bauble/plugins/report/__init__.py +++ b/bauble/plugins/report/__init__.py @@ -25,6 +25,7 @@ import logging import os import traceback +from typing import Any logger = logging.getLogger(__name__) @@ -73,7 +74,7 @@ # to be populated by the dialog box, with fields mentioned in the template -options = {} +options: dict[str, Any] = {} def _pertinent_objects_generator(query, order_by): @@ -564,7 +565,7 @@ def resize(self): class ReportToolDialogPresenter: - formatter_class_map = {} # title->class map + formatter_class_map: dict[str, type] = {} # title->class map def __init__(self, view): self.view = view diff --git a/bauble/plugins/report/mako/__init__.py b/bauble/plugins/report/mako/__init__.py index e8e99410e..df3945c41 100644 --- a/bauble/plugins/report/mako/__init__.py +++ b/bauble/plugins/report/mako/__init__.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) from gi.repository import Gtk # noqa -from mako.template import Template +from mako.template import Template # type: ignore [import-untyped] from bauble import paths from bauble import utils diff --git a/bauble/plugins/synclone/sync.py b/bauble/plugins/synclone/sync.py index ebff3fe6b..9c9b2675e 100644 --- a/bauble/plugins/synclone/sync.py +++ b/bauble/plugins/synclone/sync.py @@ -97,7 +97,12 @@ def add_batch_from_uri(cls, uri: str | URL) -> str: logger.debug("adding batch form uri: %s", uri) clone_engine: Engine = create_engine(uri) - batch_num: str = meta.get_default("sync_batch_num", 1).value + batch_num_meta = meta.get_default("sync_batch_num", "1") + + if not batch_num_meta: + raise error.DatabaseError("Not connected to a database") + + batch_num = batch_num_meta.value or "" history: Table = db.History.__table__ meta_table: Table = meta.BaubleMeta.__table__ @@ -496,9 +501,10 @@ def __init__(self, uri: str | URL | None = None) -> None: super().__init__() self._uri = None self.uri = uri # type: ignore [assignment] - self.last_pos: tuple[ - Gtk.TreePath | None, Gtk.TreeViewColumn | None, int, int - ] | None = None + self.last_pos: ( + tuple[Gtk.TreePath | None, Gtk.TreeViewColumn | None, int, int] + | None + ) = None self.setup_context_menu() @property @@ -581,7 +587,7 @@ def on_select_batch(self, *_args) -> None: ) selection = self.sync_tv.get_selection() # pylint: disable=not-an-iterable - for row in self.liststore: # type: ignore [attr-defined] + for row in self.liststore: if row[self.TVC_BATCH] == batch_num: selection.select_iter(row.iter) @@ -598,7 +604,7 @@ def on_select_related(self, *_args) -> None: ) selection = self.sync_tv.get_selection() # pylint: disable=not-an-iterable - for row in self.liststore: # type: ignore [attr-defined] + for row in self.liststore: row_obj = row[self.TVC_OBJ] if ( row_obj.table_name == obj.table_name @@ -671,7 +677,7 @@ def on_sync_selected_btn_clicked(self, *_args) -> None: selection = self.sync_tv.get_selection() selection.unselect_all() # pylint: disable=not-an-iterable - for row in self.liststore: # type: ignore [attr-defined] + for row in self.liststore: if row[self.TVC_OBJ].id in failed: selection.select_iter(row.iter) elif self.uri: @@ -773,9 +779,9 @@ def update(self, *args) -> None: logger.debug("selecting batch num: %s", batch_num) selection = self.sync_tv.get_selection() # pylint: disable=not-an-iterable - for row in self.liststore: # type: ignore [attr-defined] - if row[self.TVC_BATCH] == batch_num: - selection.select_iter(row.iter) + for tree_row in self.liststore: + if tree_row[self.TVC_BATCH] == batch_num: + selection.select_iter(tree_row.iter) class ResolveCommandHandler(pluginmgr.CommandHandler): diff --git a/bauble/plugins/tag/__init__.py b/bauble/plugins/tag/__init__.py index eb4edfa02..65dedbdef 100755 --- a/bauble/plugins/tag/__init__.py +++ b/bauble/plugins/tag/__init__.py @@ -587,7 +587,7 @@ class Tag(db.Base): description = Column(UnicodeText) # relations - objects_ = relationship( + objects_: list["TaggedObj"] = relationship( "TaggedObj", cascade="all, delete-orphan", backref="tag" ) diff --git a/bauble/plugins/tag/test_tag.py b/bauble/plugins/tag/test_tag.py index 197bc263a..93003e01c 100644 --- a/bauble/plugins/tag/test_tag.py +++ b/bauble/plugins/tag/test_tag.py @@ -81,7 +81,7 @@ def setUp_data(): utils.reset_sequence(col) -setUp_data.order = 2 +setUp_data.order = 2 # type: ignore [attr-defined] def test_duplicate_ids(): diff --git a/bauble/query_builder.py b/bauble/query_builder.py index 096bb7a75..8704639f8 100644 --- a/bauble/query_builder.py +++ b/bauble/query_builder.py @@ -263,7 +263,7 @@ class ExpressionRow: "like", "contains", ] - custom_columns = {} + custom_columns: dict[str, tuple] = {} def __init__(self, query_builder, remove_callback, row_number): self.proptype = None @@ -713,18 +713,18 @@ def __init__(self, search_string): self.is_valid = False @property - def clauses(self): + def clauses(self) -> list: if not self.__clauses: from dataclasses import dataclass # pylint: disable=too-few-public-methods @dataclass class Clause: - not_: str = None - connector: str = None - field: str = None - operator: str = None - value: str = None + not_: str | None = None + connector: str | None = None + field: str | None = None + operator: str | None = None + value: str | None = None self.__clauses = [] for part in [k for k in self.parsed if len(k) > 0][2:]: @@ -753,7 +753,7 @@ def domain(self): class QueryBuilder(GenericEditorPresenter): view_accept_buttons = ["cancel_button", "confirm_button"] - default_size = [] + default_size: tuple[int, ...] = () def __init__(self, view=None): super().__init__(self, view=view, refresh_view=False, session=False) diff --git a/bauble/search/clauses.py b/bauble/search/clauses.py index e9d92be06..f0866fc15 100644 --- a/bauble/search/clauses.py +++ b/bauble/search/clauses.py @@ -60,7 +60,7 @@ AGGREGATE_FUNC_NAMES = ["sum", "avg", "min", "max", "count", "total"] -Q = typing.TypeVar("Q", Query, Select) +Q = typing.TypeVar("Q", Select, Query) @dataclass @@ -313,17 +313,19 @@ def evaluate(self, handler: QueryHandler) -> Query | Select: # start a new query if isinstance(handler.query, Query): query = handler.session.query(handler.domain) - or_handler = QueryHandler( - handler.session, handler.domain, query + handler.query = handler.query.union( + operand.evaluate( + QueryHandler(handler.session, handler.domain, query) + ) ) else: select_ = select(handler.domain.id) # type: ignore[attr-defined] # noqa - or_handler = QueryHandler( - handler.session, handler.domain, select_ + handler.query = handler.query.union( + operand.evaluate( + QueryHandler(handler.session, handler.domain, select_) + ) ) - handler.query = typing.cast( - Query, handler.query.union(operand.evaluate(or_handler)) - ) + return handler.query diff --git a/bauble/test/test_connmgr.py b/bauble/test/test_connmgr.py index dc4b88489..2d59e89d8 100644 --- a/bauble/test/test_connmgr.py +++ b/bauble/test/test_connmgr.py @@ -57,9 +57,6 @@ def test_duplicate_ids(): assert not check_dupids(os.path.join(head, "connmgr.glade")) -prefs.testing = True - - class ConnMgrPresenterTests(BaubleTestCase): "Presenter manages view and model, implements view callbacks." diff --git a/bauble/test/test_db.py b/bauble/test/test_db.py index d293d768b..47dedf91f 100644 --- a/bauble/test/test_db.py +++ b/bauble/test/test_db.py @@ -43,10 +43,6 @@ from bauble.test import BaubleTestCase from bauble.test import get_setUp_data_funcs -prefs.testing = True - -# db.sqlalchemy_debug(True) - class HistoryTests(BaubleTestCase): def test_history_add_insert_populates(self): diff --git a/bauble/test/test_prefs.py b/bauble/test/test_prefs.py index 959fcf894..c623de0b4 100644 --- a/bauble/test/test_prefs.py +++ b/bauble/test/test_prefs.py @@ -365,10 +365,9 @@ def test_on_prefs_insert_activated_starts_dialog(self, mock_dialog): prefs_view.on_prefs_insert_activate(None, None) mock_dialog.assert_called() - def test_on_prefs_edit_toggled(self): - from bauble import utils + @mock.patch("bauble.utils.yes_no_dialog") + def test_on_prefs_edit_toggled(self, mock_dialog): - orig_yes_no_dialog = utils.yes_no_dialog prefs_view = prefs.PrefsView() # starts without editing @@ -376,7 +375,7 @@ def test_on_prefs_edit_toggled(self): self.assertIsNone(prefs_view.button_press_id) # toggle editing to True with yes to dialog - utils.yes_no_dialog = lambda x, parent: True + mock_dialog.return_value = True prefs_view.prefs_edit_chkbx.set_active(True) prefs_view.on_prefs_edit_toggled(prefs_view.prefs_edit_chkbx) @@ -391,16 +390,15 @@ def test_on_prefs_edit_toggled(self): self.assertIsNone(prefs_view.button_press_id) # toggle editing to True with no to dialog - utils.yes_no_dialog = lambda x, parent: False + mock_dialog.return_value = False prefs_view.prefs_edit_chkbx.set_active(True) prefs_view.on_prefs_edit_toggled(prefs_view.prefs_edit_chkbx) self.assertFalse(prefs_view.prefs_data_renderer.props.editable) self.assertIsNone(prefs_view.button_press_id) - utils.yes_no_dialog = orig_yes_no_dialog - - def test_on_prefs_edited(self): + @mock.patch("bauble.utils.yes_no_dialog") + def test_on_prefs_edited(self, mock_dialog): key = "bauble.keys" prefs.prefs[key] = True prefs_view = prefs.PrefsView() @@ -433,13 +431,9 @@ def test_on_prefs_edited(self): self.assertEqual(prefs.prefs[key], {"this": "that"}) # delete option - from bauble import utils - - orig_yes_no_dialog = utils.yes_no_dialog - utils.yes_no_dialog = lambda x, parent: True + mock_dialog.return_value = True prefs_view.on_prefs_edited(None, path, "") self.assertIsNone(prefs.prefs[key]) - utils.yes_no_dialog = orig_yes_no_dialog @mock.patch( "bauble.prefs.Gtk.MessageDialog.run", return_value=Gtk.ResponseType.OK diff --git a/bauble/test/test_search.py b/bauble/test/test_search.py index a56d665fd..f2af4abf7 100644 --- a/bauble/test/test_search.py +++ b/bauble/test/test_search.py @@ -37,9 +37,6 @@ from bauble.test import BaubleTestCase from bauble.test import get_setUp_data_funcs -prefs.testing = True - - parser = search.parser diff --git a/bauble/utils/__init__.py b/bauble/utils/__init__.py index 58590a200..0886c08a5 100755 --- a/bauble/utils/__init__.py +++ b/bauble/utils/__init__.py @@ -39,11 +39,12 @@ from pathlib import Path from typing import Any from typing import Union +from typing import cast from xml.sax import saxutils logger = logging.getLogger(__name__) -from gi.repository import GdkPixbuf # type: ignore [import-untyped] +from gi.repository import GdkPixbuf from gi.repository import GLib from gi.repository import Gtk from pyparsing import Group @@ -195,8 +196,11 @@ def __init__( def callback(self) -> None: pixbuf = self.loader.get_pixbuf() if not pixbuf: + # type guard return pixbuf = pixbuf.apply_embedded_orientation() + if not pixbuf: + return scale_x = pixbuf.get_width() / 400 scale_y = pixbuf.get_height() / 400 scale = max(scale_x, scale_y, 1) @@ -204,7 +208,7 @@ def callback(self) -> None: y = int(pixbuf.get_height() / scale) scaled_buf = pixbuf.scale_simple(x, y, GdkPixbuf.InterpType.BILINEAR) if self.box.get_children(): - image = self.box.get_children()[0] + image = cast(Gtk.Image, self.box.get_children()[0]) else: image = Gtk.Image() self.box.pack_start(image, True, True, 0) @@ -365,7 +369,7 @@ class BuilderLoader: # this class # http://bugzilla.gnome.org/show_bug.cgi?id=589057,560822 - builders = {} + builders: dict[str, Gtk.Builder] = {} @classmethod def load(cls, filename): @@ -1559,7 +1563,7 @@ def show(self, *_args, **_kwargs): class MessageBox(GenericMessageBox): - """A MessageBox that can display a message label at the top of an editor.""" + """A MessageBox that can display a message label at the top of an editor""" def __init__(self, msg=None, details=None): super().__init__() diff --git a/bauble/utils/desktop.py b/bauble/utils/desktop.py index a19f33d15..2964b584e 100755 --- a/bauble/utils/desktop.py +++ b/bauble/utils/desktop.py @@ -71,54 +71,34 @@ __version__ = "0.2.4" import os +import subprocess import sys -# Provide suitable process creation functions. - +from bauble import utils -try: - import subprocess +# Provide suitable process creation functions. - def _run(cmd, shell, wait): - opener = subprocess.Popen(cmd, shell=shell) - if wait: - opener.wait() - return opener.pid - def _readfrom(cmd, shell): - opener = subprocess.Popen( - cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - opener.stdin.close() - return opener.stdout.read() - - def _status(cmd, shell): - opener = subprocess.Popen(cmd, shell=shell) +def _run(cmd, shell, wait): + opener = subprocess.Popen(cmd, shell=shell) + if wait: opener.wait() - return opener.returncode == 0 - -except ImportError: - import popen2 + return opener.pid - def _run(cmd, shell, wait): - opener = popen2.Popen3(cmd) - if wait: - opener.wait() - return opener.pid - def _readfrom(cmd, shell): - opener = popen2.Popen3(cmd) - opener.tochild.close() - opener.childerr.close() - return opener.fromchild.read() +def _readfrom(cmd, shell): + opener = subprocess.Popen( + cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + opener.stdin.close() + return opener.stdout.read() - def _status(cmd, shell): - opener = popen2.Popen3(cmd) - opener.wait() - return opener.poll() == 0 +def _status(cmd, shell): + opener = subprocess.Popen(cmd, shell=shell) + opener.wait() + return opener.returncode == 0 -import subprocess # # Private functions. @@ -250,7 +230,6 @@ def open(url, desktop=None, wait=0.5, dialog_on_error=False): """ # Decide on the desktop environment in use. - from bauble import utils desktop_in_use = use_desktop(desktop) cmd = None diff --git a/bauble/utils/geo.py b/bauble/utils/geo.py index caa2a5b55..538070886 100644 --- a/bauble/utils/geo.py +++ b/bauble/utils/geo.py @@ -29,7 +29,7 @@ import tempfile -from mako.template import Template +from mako.template import Template # type: ignore [import-untyped] from pyproj import ProjError from pyproj import Transformer from sqlalchemy import Column diff --git a/bauble/view.py b/bauble/view.py index e4717ae9d..489892692 100755 --- a/bauble/view.py +++ b/bauble/view.py @@ -519,7 +519,7 @@ def multiproc_counter(url, klass, ids): # process produced. The below suppresses the icon AFTER it has already # popped up meaning you get a bunch of icons appearing for a # around a second and then disappearing. - import AppKit + import AppKit # type: ignore [import-untyped] AppKit.NSApp.setActivationPolicy_(1) # 2 also works db.open_conn(url) @@ -700,9 +700,7 @@ def __init__(self, parent: Gtk.Paned, pic_pane: Gtk.Paned) -> None: self.parent = parent self.pic_pane = pic_pane self.set_width_and_notebook_page() - self.restore_position: int | None = prefs.prefs.get( - PIC_PANE_WIDTH_PREF, -1 - ) + self.restore_position: int = prefs.prefs.get(PIC_PANE_WIDTH_PREF, -1) self.restore_pic_pane = False pic_pane.show_all() self.pic_pane.set_position(self.restore_position) @@ -722,7 +720,7 @@ def __init__(self, parent: Gtk.Paned, pic_pane: Gtk.Paned) -> None: self.waiting_on_realise = 0 self.selection: list[db.Base] = [] # fires considerably less than child1 or pic_pane itself. - pic_pane.get_child2().connect( + cast(Gtk.Widget, pic_pane.get_child2()).connect( "size-allocate", self.on_pic_pane_size_allocation ) self._set_pic_pane_pos_timer_id = None @@ -1500,7 +1498,7 @@ def on_copy_selection(self, _action, _param): return None out = [] - from mako.template import Template + from mako.template import Template # type: ignore [import-untyped] try: for value in selected_values: diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..7b85dc7d2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,20 @@ +# linters, etc. +# NOTE to use: pygobject-stubs requires setting PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3,Soup2 +# i.e. +# $ PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3,Soup2 pip install -r requirements-dev.txt +mypy +pygobject-stubs +types-python-dateutil +sqlalchemy[mypy]==1.4.54 +Babel +lxml-stubs +types-psycopg2 +types-babel +pyparsing[diagrams] +pylint +flake8 +pylint-sqlalchemy +black +isort +coverage +pytest