From 8d521c7e0f11d93a2f1d95054e769c1b6ebf3fef Mon Sep 17 00:00:00 2001 From: Nathan Merritt Date: Thu, 8 Sep 2022 16:35:40 -0600 Subject: [PATCH] start paring down list via mypy.ini and fixing errors, 6 files to go after this commit --- .flake8 | 8 ++++++ .github/workflows/test.yml | 2 +- BAC0/__init__.py | 2 +- BAC0/core/app/ScriptApplication.py | 3 ++- BAC0/core/devices/Trends.py | 22 +--------------- BAC0/core/devices/local/object.py | 26 ++++--------------- BAC0/core/devices/mixins/read_mixin.py | 6 ++--- .../functions/DeviceCommunicationControl.py | 11 +++----- BAC0/core/functions/Reinitialize.py | 11 +++----- BAC0/core/functions/TimeSync.py | 2 +- BAC0/core/io/Write.py | 4 +-- BAC0/core/utils/notes.py | 5 ++-- BAC0/db/influxdb.py | 6 ++--- BAC0/db/sql.py | 4 +-- BAC0/scripts/Base.py | 6 +++-- BAC0/scripts/Complete.py | 9 +++---- BAC0/scripts/Lite.py | 14 +++++----- BAC0/tasks/Devices.py | 7 ----- BAC0/tasks/Poll.py | 2 +- BAC0/tasks/TaskManager.py | 8 +++--- BAC0/web/BokehRenderer.py | 17 +++--------- mypy.ini | 7 +++++ requirements-mypy.txt | 2 ++ 23 files changed, 71 insertions(+), 113 deletions(-) create mode 100644 .flake8 create mode 100644 mypy.ini create mode 100644 requirements-mypy.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..cd6d1983 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +ignore = W291,W503,E265,E266,E302,E722 +max-line-length = 127 +exclude = + .git, + __pycache__, + .mypy_cache, + .pytest_cache, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcd74171..b9a0b106 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, 3.10] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/BAC0/__init__.py b/BAC0/__init__.py index c38bb604..947a4196 100644 --- a/BAC0/__init__.py +++ b/BAC0/__init__.py @@ -54,7 +54,7 @@ connect = gui else: - connect = lite + connect = lite # type: ignore[assignment, misc] web = lambda: print( "All features not available to run BAC0.web(). Some modules are missing (flask, flask-bootstrap, bokeh, pandas). See docs for details. To start BAC0, use BAC0.lite()" ) diff --git a/BAC0/core/app/ScriptApplication.py b/BAC0/core/app/ScriptApplication.py index e2239078..0a670bf4 100644 --- a/BAC0/core/app/ScriptApplication.py +++ b/BAC0/core/app/ScriptApplication.py @@ -18,6 +18,7 @@ """ # --- standard Python modules --- from collections import defaultdict +import typing as t # --- 3rd party modules --- from bacpypes.app import ApplicationIOController @@ -224,7 +225,7 @@ def __init__( # bind the BIP stack to the network, no network number self.nsap.bind(self.bip, address=self.localAddress) - self.i_am_counter = defaultdict(int) + self.i_am_counter: t.Dict[t.Tuple[str, int], int] = defaultdict(int) self.i_have_counter = defaultdict(int) self.who_is_counter = defaultdict(int) diff --git a/BAC0/core/devices/Trends.py b/BAC0/core/devices/Trends.py index 31628e61..d8164a07 100755 --- a/BAC0/core/devices/Trends.py +++ b/BAC0/core/devices/Trends.py @@ -4,37 +4,18 @@ # Copyright (C) 2015 by Christian Tremblay, P.Eng # Licensed under LGPLv3, see file LICENSE in this source tree. # -""" -Points.py - Definition of points so operations on Read results are more convenient. -""" # --- standard Python modules --- -from datetime import datetime -from collections import namedtuple -import time -from itertools import islice - - # --- 3rd party modules --- try: import pandas as pd - from pandas.io import sql - - try: - from pandas import Timestamp - except ImportError: - from pandas.lib import Timestamp _PANDAS = True except ImportError: _PANDAS = False -from bacpypes.object import TrendLogObject from bacpypes.primitivedata import Date, Time # --- this application's modules --- -from ...tasks.Poll import SimplePoll as Poll -from ...tasks.Match import Match, Match_Value -from ..io.IOExceptions import NoResponseFromController, UnknownPropertyError from ..utils.notes import note_and_log @@ -56,7 +37,6 @@ def __init__(self): self.record_count = 0 self.total_record_count = 0 self.log_interval = 0 - self.description = None self.statusFlags = None self.status_flags = { "in_alarm": False, @@ -196,7 +176,7 @@ def create_dataframe(self, log_buffer): @property def history(self): self.read_log_buffer() - if not _PANDAS: + if not _PANDAS or self.properties._df is None: return dict( zip( self.properties._history_components["index"], diff --git a/BAC0/core/devices/local/object.py b/BAC0/core/devices/local/object.py index b0be58e5..c1ca36cc 100644 --- a/BAC0/core/devices/local/object.py +++ b/BAC0/core/devices/local/object.py @@ -7,30 +7,14 @@ BAC0BBMDDeviceApplication, BAC0ForeignDeviceApplication, ) - -from bacpypes.object import ( - AnalogInputObject, - AnalogValueObject, - BinaryValueObject, - Property, - register_object_type, - registered_object_types, - DatePatternValueObject, - ReadableProperty, - WritableProperty, - OptionalProperty, -) from bacpypes.basetypes import ( - EngineeringUnits, - DateTime, PriorityArray, - StatusFlags, Reliability, - Polarity, ) +import typing as t from collections import namedtuple -from colorama import Fore, Back, Style +from colorama import Fore @note_and_log @@ -55,14 +39,14 @@ class ObjectFactory(object): """ - instances = {} + instances: t.Dict[str, t.Set] = {} - definition = namedtuple( + definition = namedtuple( # type: ignore[name-match] "Definition", "name, objectType, instance, properties, description, presentValue, is_commandable, relinquish_default", ) - objects = {} + objects: t.Dict[str, t.Any] = {} # In the future... should think about a way to store relinquish default values because on a restart # those should be restored. diff --git a/BAC0/core/devices/mixins/read_mixin.py b/BAC0/core/devices/mixins/read_mixin.py index 56ff1903..3b020c7f 100755 --- a/BAC0/core/devices/mixins/read_mixin.py +++ b/BAC0/core/devices/mixins/read_mixin.py @@ -8,13 +8,13 @@ read_mixin.py - Add ReadProperty and ReadPropertyMultiple to a device """ # --- standard Python modules --- +import typing as t # --- 3rd party modules --- # --- this application's modules --- from ....tasks.Poll import DeviceNormalPoll, DeviceFastPoll from ...io.IOExceptions import ( - ReadPropertyMultipleException, NoResponseFromController, SegmentationNotSupported, BufferOverflow, @@ -24,7 +24,6 @@ BooleanPoint, EnumPoint, StringPoint, - OfflinePoint, DateTimePoint, ) from ..Trends import TrendLog @@ -565,6 +564,7 @@ def poll(self, command="start", *, delay=10): device.poll('stop') device.poll(delay = 5) """ + _poll_cls: t.Union[t.Type[DeviceFastPoll], t.Type[DeviceNormalPoll]] if delay < 10: self.properties.fast_polling = True _poll_cls = DeviceFastPoll @@ -580,7 +580,7 @@ def poll(self, command="start", *, delay=10): if ( str(command).lower() == "stop" - or command == False + or command == False # noqa E712 or command == 0 or delay == 0 ): diff --git a/BAC0/core/functions/DeviceCommunicationControl.py b/BAC0/core/functions/DeviceCommunicationControl.py index f936aa9c..767adb96 100644 --- a/BAC0/core/functions/DeviceCommunicationControl.py +++ b/BAC0/core/functions/DeviceCommunicationControl.py @@ -9,12 +9,10 @@ """ # --- standard Python modules --- -import datetime as dt # --- 3rd party modules --- -from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.primitivedata import Date, Time, CharacterString, Unsigned16 -from bacpypes.basetypes import DateTime +from bacpypes.pdu import Address +from bacpypes.primitivedata import CharacterString, Unsigned16 from bacpypes.apdu import ( DeviceCommunicationControlRequest, DeviceCommunicationControlRequestEnableDisable, @@ -25,9 +23,6 @@ from ...core.io.Read import find_reason from ..io.IOExceptions import ( - SegmentationNotSupported, - ReadPropertyException, - ReadPropertyMultipleException, NoResponseFromController, ApplicationNotStarted, ) @@ -81,7 +76,7 @@ def dcc(self, address=None, duration=None, password=None, state=None): if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( - "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) + "Not an ack. | APDU : {} / {}".format(apdu, type(apdu)) ) return diff --git a/BAC0/core/functions/Reinitialize.py b/BAC0/core/functions/Reinitialize.py index 513f388d..6f5b5519 100644 --- a/BAC0/core/functions/Reinitialize.py +++ b/BAC0/core/functions/Reinitialize.py @@ -10,21 +10,16 @@ """ from ...core.io.Read import find_reason from ..io.IOExceptions import ( - SegmentationNotSupported, - ReadPropertyException, - ReadPropertyMultipleException, NoResponseFromController, ApplicationNotStarted, ) from ...core.utils.notes import note_and_log # --- standard Python modules --- -import datetime as dt # --- 3rd party modules --- -from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.primitivedata import Date, Time, CharacterString -from bacpypes.basetypes import DateTime +from bacpypes.pdu import Address +from bacpypes.primitivedata import CharacterString from bacpypes.apdu import ( ReinitializeDeviceRequest, ReinitializeDeviceRequestReinitializedStateOfDevice, @@ -74,7 +69,7 @@ def reinitialize(self, address=None, password=None, state="coldstart"): if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( - "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) + "Not an ack. | APDU : {} / {}".format(apdu, type(apdu)) ) return diff --git a/BAC0/core/functions/TimeSync.py b/BAC0/core/functions/TimeSync.py index 54ec67ba..b35c3c07 100644 --- a/BAC0/core/functions/TimeSync.py +++ b/BAC0/core/functions/TimeSync.py @@ -157,7 +157,7 @@ def local_date(self): def utcOffset(self) -> float: "Returns UTC offset in minutes" - return round(self.now.astimezone().utcoffset().total_seconds() / 60) + return round(self.now.astimezone().utcoffset().total_seconds() / 60) # type: ignore[union-attr] def is_dst(self) -> bool: return self.timezone.dst(self.now) != dt.timedelta(0) diff --git a/BAC0/core/io/Write.py b/BAC0/core/io/Write.py index bea281db..92bc49bf 100644 --- a/BAC0/core/io/Write.py +++ b/BAC0/core/io/Write.py @@ -103,7 +103,7 @@ def write(self, args, vendor_id=0, timeout=10): if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( - "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) + "Not an ack. | APDU : {} / {}".format(apdu, type(apdu)) ) return @@ -280,7 +280,7 @@ def writeMultiple(self, addr=None, args=None, vendor_id=0, timeout=10): if not isinstance(apdu, SimpleAckPDU): # expect an ACK self._log.warning("Not an ack, see debug for more infos.") self._log.debug( - "Not an ack. | APDU : {} / {}".format((apdu, type(apdu))) + "Not an ack. | APDU : {} / {}".format(apdu, type(apdu)) ) return diff --git a/BAC0/core/utils/notes.py b/BAC0/core/utils/notes.py index 6f53ae9b..797c0f85 100644 --- a/BAC0/core/utils/notes.py +++ b/BAC0/core/utils/notes.py @@ -10,8 +10,9 @@ from collections import namedtuple from datetime import datetime import logging -from logging import FileHandler +from logging import FileHandler, Logger import sys +import typing as t import os from os.path import expanduser, join @@ -26,7 +27,7 @@ class LogList: - LOGGERS = [] + LOGGERS: t.List[Logger] = [] def convert_level(level): diff --git a/BAC0/db/influxdb.py b/BAC0/db/influxdb.py index 21e85c22..c084d5ce 100644 --- a/BAC0/db/influxdb.py +++ b/BAC0/db/influxdb.py @@ -1,11 +1,10 @@ try: from influxdb_client import InfluxDBClient, Point, WriteOptions - + from influxdb_client.client.write import WriteApi except ImportError: raise ImportError("Install influxdb to use this feature") import pytz -from datetime import datetime class InfluxDB: @@ -21,7 +20,8 @@ class InfluxDB: tags_file = None username = None password = None - client = None + client: InfluxDBClient + write_api: WriteApi def __init__(self, params): # params should be a dict with name=InfluxDB and bucket=valid_bucket_to_use. diff --git a/BAC0/db/sql.py b/BAC0/db/sql.py index 4d19e4a3..87c0c6dc 100644 --- a/BAC0/db/sql.py +++ b/BAC0/db/sql.py @@ -5,7 +5,7 @@ # Licensed under LGPLv3, see file LICENSE in this source tree. # """ -sql.py - +sql.py - """ # --- standard Python modules --- @@ -271,7 +271,7 @@ def his_from_sql(self, db_name, point): """ Retrive point histories from SQL database """ - his = self._read_from_sql('select * from "{}"'.format("history", db_name)) + his = self._read_from_sql('select * from "{}"'.format("history"), db_name) his.index = his["index"].apply(Timestamp) return his.set_index("index")[point] diff --git a/BAC0/scripts/Base.py b/BAC0/scripts/Base.py index ccbb27f7..9964d3f5 100644 --- a/BAC0/scripts/Base.py +++ b/BAC0/scripts/Base.py @@ -21,6 +21,7 @@ def stopApp() """ import random import sys +import typing as t # --- standard Python modules --- from threading import Thread @@ -31,6 +32,7 @@ def stopApp() from bacpypes.core import stop as stopBacnetIPApp from bacpypes.local.device import LocalDeviceObject from bacpypes.primitivedata import CharacterString +from bacpypes.pdu import Address # --- this application's modules --- from .. import infos @@ -94,7 +96,7 @@ class Base: :param segmentationSupported='segmentedBoth': """ - _used_ips = set() + _used_ips: t.Set[Address] = set() def __init__( self, @@ -164,7 +166,7 @@ def __init__( self.modelName = modelName self.description = description - self.discoveredDevices = None + self.discoveredDevices: t.Optional[t.Dict[t.Tuple[str, int], int]] = None self.systemStatus = DeviceStatus(1) self.bbmdAddress = bbmdAddress diff --git a/BAC0/scripts/Complete.py b/BAC0/scripts/Complete.py index 27337dc6..9d19396d 100644 --- a/BAC0/scripts/Complete.py +++ b/BAC0/scripts/Complete.py @@ -26,6 +26,7 @@ class ReadWriteScript(BasicScript,ReadProperty,WriteProperty) import logging import pandas as pd import time +import typing as t # --- 3rd party modules --- @@ -61,9 +62,7 @@ class Stats_Mixin: def number_of_devices(self): if not self.discoveredDevices: return 0 - s = [] - [s.append(x) for x in self.discoveredDevices.items() if x[1] > 0] - return len(s) + return len([x for x in self.discoveredDevices.items() if x[1] > 0]) @property def number_of_registered_trends(self): @@ -105,7 +104,7 @@ def network_stats(self): """ Used by Flask to show informations on the network """ - statistics = {} + statistics: t.Dict[str, t.Any] = {} mstp_networks = [] mstp_map = {} ip_devices = [] @@ -190,7 +189,7 @@ def __init__( @property def devices(self): lst = [] - for device in list(self.discoveredDevices): + for device in list(self.discoveredDevices or {}): try: deviceName, vendorName = self.readMultiple( "{} device {} objectName vendorName".format(device[0], device[1]) diff --git a/BAC0/scripts/Lite.py b/BAC0/scripts/Lite.py index dc72a1bd..760db513 100755 --- a/BAC0/scripts/Lite.py +++ b/BAC0/scripts/Lite.py @@ -128,16 +128,18 @@ def __init__( if ping: self._ping_task.start() - if ip is None: + if ip is None and port is not None: host = HostIP(port) ip_addr = host.address else: try: ip, subnet_mask_and_port = ip.split("/") try: - mask, port = subnet_mask_and_port.split(":") + mask_s, port_s = subnet_mask_and_port.split(":") + mask = int(mask_s) + port = int(port_s) except ValueError: - mask = subnet_mask_and_port + mask = int(subnet_mask_and_port) except ValueError: ip = ip @@ -265,7 +267,7 @@ def discover( elif networks == "known": _networks = self.known_network_numbers.copy() else: - if networks < 65535: + if isinstance(networks, int) and networks < 65535: _networks.append(networks) if _networks: @@ -402,7 +404,7 @@ def remove_trend(self, point_to_remove: t.Union[Point, TrendLog, VirtualPoint]) del self._points_to_trend[oid] @property - def devices(self) -> t.List[t.Tuple[str, str, t.Any, t.Any]]: + def devices(self) -> t.List[t.Tuple[float, float, str, int]]: """ This property will create a good looking table of all the discovered devices seen on the network. @@ -411,7 +413,7 @@ def devices(self) -> t.List[t.Tuple[str, str, t.Any, t.Any]]: manufacturer, etc and in big network, this could be a long process. """ lst = [] - for device in list(self.discoveredDevices): + for device in list(self.discoveredDevices or {}): try: deviceName, vendorName = self.readMultiple( "{} device {} objectName vendorName".format(device[0], device[1]) diff --git a/BAC0/tasks/Devices.py b/BAC0/tasks/Devices.py index 5a148c8b..e8d64e71 100644 --- a/BAC0/tasks/Devices.py +++ b/BAC0/tasks/Devices.py @@ -4,12 +4,6 @@ # Copyright (C) 2015 by Christian Tremblay, P.Eng # Licensed under LGPLv3, see file LICENSE in this source tree. # -""" -Match.py - verify a point's status matches its commanded value. - -Example: - Is a fan commanded to 'On' actually 'running'? -""" # --- standard Python modules --- # --- 3rd party modules --- @@ -18,7 +12,6 @@ from .TaskManager import Task from ..core.io.IOExceptions import BadDeviceDefinition from ..core.devices.Device import Device -from ..core.utils.notes import note_and_log """ A way to define a BAC0.device using a task, so it won't block the Notebook or the REPL diff --git a/BAC0/tasks/Poll.py b/BAC0/tasks/Poll.py index fee8ea7d..7008fbfc 100644 --- a/BAC0/tasks/Poll.py +++ b/BAC0/tasks/Poll.py @@ -80,7 +80,7 @@ def __init__(self, device: Union[RPMDeviceConnected, RPDeviceConnected], self._counter = 0 @property - def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected]: + def device(self) -> Union[RPMDeviceConnected, RPDeviceConnected, None]: return self._device() def task(self) -> None: diff --git a/BAC0/tasks/TaskManager.py b/BAC0/tasks/TaskManager.py index d42cd834..96de2a56 100644 --- a/BAC0/tasks/TaskManager.py +++ b/BAC0/tasks/TaskManager.py @@ -5,7 +5,7 @@ # Licensed under LGPLv3, see file LICENSE in this source tree. # """ -TaskManager.py - creation of threads used for repetitive tasks. +TaskManager.py - creation of threads used for repetitive tasks. A key building block for point simulation. """ @@ -62,7 +62,7 @@ def process(cls): time.sleep(1) except DeviceNotConnected as error: cls._log.warning( - "Device disconnected. Removing task ({}).".format(error, task) + "Device disconnected with error {}. Removing task ({}).".format(error, task) ) cls.tasks.remove(task.id) except Exception as error: @@ -150,7 +150,7 @@ def __init__(self, fn=None, name=None, delay=0): self.average_execution_delay = 0 self.average_latency = 0 self.next_execution = time.time() + delay + (random() * 10) - self.execution_time = 0 + self.execution_time = 0.0 self.count = 0 self.id = id(self) self._kwargs = None @@ -171,7 +171,7 @@ def execute(self): self.fn() else: if self._kwargs is not None: - self.task(self._kwargs) + self.task(**self._kwargs) else: self.task() if self.previous_execution: diff --git a/BAC0/web/BokehRenderer.py b/BAC0/web/BokehRenderer.py index 0030e5be..a0730526 100644 --- a/BAC0/web/BokehRenderer.py +++ b/BAC0/web/BokehRenderer.py @@ -13,36 +13,25 @@ ColumnDataSource, HoverTool, Range1d, - Label, - LabelSet, LinearAxis, Legend, LegendItem, - Dropdown, MultiChoice, CustomJS, ) -from bokeh.models.widgets import DataTable, TableColumn, Div -from bokeh.layouts import widgetbox, row, column, gridplot, widgetbox -from bokeh.palettes import d3, viridis +from bokeh.models.widgets import DataTable, TableColumn +from bokeh.layouts import row, column +from bokeh.palettes import d3 from bokeh.io import curdoc from bokeh.application.handlers import Handler -from bokeh.models.tools import CustomJSHover -from functools import partial -from tornado import gen - -import math import pandas as pd import weakref from queue import Queue -from collections import deque -from ..tasks.RecurringTask import RecurringTask from ..core.utils.notes import note_and_log from ..core.devices.Virtuals import VirtualPoint -from ..core.devices.Trends import TrendLog @note_and_log diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..538c4601 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +ignore_missing_imports = True +check_untyped_defs = True +warn_unused_configs = True +warn_return_any = False +disable_error_code = attr-defined,union-attr,var-annotated,misc +files = BAC0/core/devices/local/object.py,BAC0/core/app/* diff --git a/requirements-mypy.txt b/requirements-mypy.txt new file mode 100644 index 00000000..b74f62de --- /dev/null +++ b/requirements-mypy.txt @@ -0,0 +1,2 @@ +mypy==0.971 +types-pytz==2022.1.1