浏览代码

directnotify: annotate types (#1527)

WMOkiishi 2 年之前
父节点
当前提交
1ca0e3f1ea

+ 35 - 27
direct/src/directnotify/DirectNotify.py

@@ -2,6 +2,10 @@
 DirectNotify module: this module contains the DirectNotify class
 DirectNotify module: this module contains the DirectNotify class
 """
 """
 
 
+from __future__ import annotations
+
+from panda3d.core import StreamWriter
+
 from . import Notifier
 from . import Notifier
 from . import Logger
 from . import Logger
 
 
@@ -12,39 +16,39 @@ class DirectNotify:
     mulitple notify categories via a dictionary of Notifiers.
     mulitple notify categories via a dictionary of Notifiers.
     """
     """
 
 
-    def __init__(self):
+    def __init__(self) -> None:
         """
         """
         DirectNotify class keeps a dictionary of Notfiers
         DirectNotify class keeps a dictionary of Notfiers
         """
         """
-        self.__categories = {}
+        self.__categories: dict[str, Notifier.Notifier] = {}
         # create a default log file
         # create a default log file
         self.logger = Logger.Logger()
         self.logger = Logger.Logger()
 
 
         # This will get filled in later by ShowBase.py with a
         # This will get filled in later by ShowBase.py with a
         # C++-level StreamWriter object for writing to standard
         # C++-level StreamWriter object for writing to standard
         # output.
         # output.
-        self.streamWriter = None
+        self.streamWriter: StreamWriter | None = None
 
 
-    def __str__(self):
+    def __str__(self) -> str:
         """
         """
         Print handling routine
         Print handling routine
         """
         """
         return "DirectNotify categories: %s" % (self.__categories)
         return "DirectNotify categories: %s" % (self.__categories)
 
 
     #getters and setters
     #getters and setters
-    def getCategories(self):
+    def getCategories(self) -> list[str]:
         """
         """
         Return list of category dictionary keys
         Return list of category dictionary keys
         """
         """
         return list(self.__categories.keys())
         return list(self.__categories.keys())
 
 
-    def getCategory(self, categoryName):
+    def getCategory(self, categoryName: str) -> Notifier.Notifier | None:
         """getCategory(self, string)
         """getCategory(self, string)
         Return the category with given name if present, None otherwise
         Return the category with given name if present, None otherwise
         """
         """
         return self.__categories.get(categoryName, None)
         return self.__categories.get(categoryName, None)
 
 
-    def newCategory(self, categoryName, logger=None):
+    def newCategory(self, categoryName: str, logger: Logger.Logger | None = None) -> Notifier.Notifier:
         """newCategory(self, string)
         """newCategory(self, string)
         Make a new notify category named categoryName. Return new category
         Make a new notify category named categoryName. Return new category
         if no such category exists, else return existing category
         if no such category exists, else return existing category
@@ -52,9 +56,11 @@ class DirectNotify:
         if categoryName not in self.__categories:
         if categoryName not in self.__categories:
             self.__categories[categoryName] = Notifier.Notifier(categoryName, logger)
             self.__categories[categoryName] = Notifier.Notifier(categoryName, logger)
             self.setDconfigLevel(categoryName)
             self.setDconfigLevel(categoryName)
-        return self.getCategory(categoryName)
+        notifier = self.getCategory(categoryName)
+        assert notifier is not None
+        return notifier
 
 
-    def setDconfigLevel(self, categoryName):
+    def setDconfigLevel(self, categoryName: str) -> None:
         """
         """
         Check to see if this category has a dconfig variable
         Check to see if this category has a dconfig variable
         to set the notify severity and then set that level. You cannot
         to set the notify severity and then set that level. You cannot
@@ -77,40 +83,42 @@ class DirectNotify:
             level = 'error'
             level = 'error'
 
 
         category = self.getCategory(categoryName)
         category = self.getCategory(categoryName)
+        assert category is not None, f'failed to find category: {categoryName!r}'
         # Note - this print statement is making it difficult to
         # Note - this print statement is making it difficult to
         # achieve "no output unless there's an error" operation - Josh
         # achieve "no output unless there's an error" operation - Josh
         # print ("Setting DirectNotify category: " + categoryName +
         # print ("Setting DirectNotify category: " + categoryName +
         #        " to severity: " + level)
         #        " to severity: " + level)
         if level == "error":
         if level == "error":
-            category.setWarning(0)
-            category.setInfo(0)
-            category.setDebug(0)
+            category.setWarning(False)
+            category.setInfo(False)
+            category.setDebug(False)
         elif level == "warning":
         elif level == "warning":
-            category.setWarning(1)
-            category.setInfo(0)
-            category.setDebug(0)
+            category.setWarning(True)
+            category.setInfo(False)
+            category.setDebug(False)
         elif level == "info":
         elif level == "info":
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(0)
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(False)
         elif level == "debug":
         elif level == "debug":
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(1)
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(True)
         else:
         else:
             print("DirectNotify: unknown notify level: " + str(level)
             print("DirectNotify: unknown notify level: " + str(level)
                    + " for category: " + str(categoryName))
                    + " for category: " + str(categoryName))
 
 
-    def setDconfigLevels(self):
+    def setDconfigLevels(self) -> None:
         for categoryName in self.getCategories():
         for categoryName in self.getCategories():
             self.setDconfigLevel(categoryName)
             self.setDconfigLevel(categoryName)
 
 
-    def setVerbose(self):
+    def setVerbose(self) -> None:
         for categoryName in self.getCategories():
         for categoryName in self.getCategories():
             category = self.getCategory(categoryName)
             category = self.getCategory(categoryName)
-            category.setWarning(1)
-            category.setInfo(1)
-            category.setDebug(1)
+            assert category is not None
+            category.setWarning(True)
+            category.setInfo(True)
+            category.setDebug(True)
 
 
     def popupControls(self, tl = None):
     def popupControls(self, tl = None):
         # Don't use a regular import, to prevent ModuleFinder from picking
         # Don't use a regular import, to prevent ModuleFinder from picking
@@ -119,5 +127,5 @@ class DirectNotify:
         NotifyPanel = importlib.import_module('direct.tkpanels.NotifyPanel')
         NotifyPanel = importlib.import_module('direct.tkpanels.NotifyPanel')
         NotifyPanel.NotifyPanel(self, tl)
         NotifyPanel.NotifyPanel(self, tl)
 
 
-    def giveNotify(self,cls):
+    def giveNotify(self, cls) -> None:
         cls.notify = self.newCategory(cls.__name__)
         cls.notify = self.newCategory(cls.__name__)

+ 14 - 10
direct/src/directnotify/Logger.py

@@ -1,27 +1,30 @@
 """Logger module: contains the logger class which creates and writes
 """Logger module: contains the logger class which creates and writes
    data to log files on disk"""
    data to log files on disk"""
 
 
+from __future__ import annotations
+
+import io
 import time
 import time
 import math
 import math
 
 
 
 
 class Logger:
 class Logger:
-    def __init__(self, fileName="log"):
+    def __init__(self, fileName: str = "log") -> None:
         """
         """
         Logger constructor
         Logger constructor
         """
         """
-        self.__timeStamp = 1
+        self.__timeStamp = True
         self.__startTime = 0.0
         self.__startTime = 0.0
-        self.__logFile = None
+        self.__logFile: io.TextIOWrapper | None = None
         self.__logFileName = fileName
         self.__logFileName = fileName
 
 
-    def setTimeStamp(self, enable):
+    def setTimeStamp(self, enable: bool) -> None:
         """
         """
         Toggle time stamp printing with log entries on and off
         Toggle time stamp printing with log entries on and off
         """
         """
         self.__timeStamp = enable
         self.__timeStamp = enable
 
 
-    def getTimeStamp(self):
+    def getTimeStamp(self) -> bool:
         """
         """
         Return whether or not we are printing time stamps with log entries
         Return whether or not we are printing time stamps with log entries
         """
         """
@@ -29,24 +32,25 @@ class Logger:
 
 
     # logging control
     # logging control
 
 
-    def resetStartTime(self):
+    def resetStartTime(self) -> None:
         """
         """
         Reset the start time of the log file for time stamps
         Reset the start time of the log file for time stamps
         """
         """
         self.__startTime = time.time()
         self.__startTime = time.time()
 
 
-    def log(self, entryString):
+    def log(self, entryString: str) -> None:
         """log(self, string)
         """log(self, string)
         Print the given string to the log file"""
         Print the given string to the log file"""
         if self.__logFile is None:
         if self.__logFile is None:
             self.__openLogFile()
             self.__openLogFile()
+        assert self.__logFile is not None
         if self.__timeStamp:
         if self.__timeStamp:
             self.__logFile.write(self.__getTimeStamp())
             self.__logFile.write(self.__getTimeStamp())
         self.__logFile.write(entryString + '\n')
         self.__logFile.write(entryString + '\n')
 
 
     # logging functions
     # logging functions
 
 
-    def __openLogFile(self):
+    def __openLogFile(self) -> None:
         """
         """
         Open a file for logging error/warning messages
         Open a file for logging error/warning messages
         """
         """
@@ -56,14 +60,14 @@ class Logger:
         logFileName = self.__logFileName + "." + st
         logFileName = self.__logFileName + "." + st
         self.__logFile = open(logFileName, "w")
         self.__logFile = open(logFileName, "w")
 
 
-    def __closeLogFile(self):
+    def __closeLogFile(self) -> None:
         """
         """
         Close the error/warning output file
         Close the error/warning output file
         """
         """
         if self.__logFile is not None:
         if self.__logFile is not None:
             self.__logFile.close()
             self.__logFile.close()
 
 
-    def __getTimeStamp(self):
+    def __getTimeStamp(self) -> str:
         """
         """
         Return the offset between current time and log file startTime
         Return the offset between current time and log file startTime
         """
         """

+ 44 - 39
direct/src/directnotify/Notifier.py

@@ -2,11 +2,16 @@
 Notifier module: contains methods for handling information output
 Notifier module: contains methods for handling information output
 for the programmer/user
 for the programmer/user
 """
 """
+
+from __future__ import annotations
+
+from .Logger import Logger
 from .LoggerGlobal import defaultLogger
 from .LoggerGlobal import defaultLogger
 from direct.showbase import PythonUtil
 from direct.showbase import PythonUtil
 from panda3d.core import ConfigVariableBool, NotifyCategory, StreamWriter, Notify
 from panda3d.core import ConfigVariableBool, NotifyCategory, StreamWriter, Notify
 import time
 import time
 import sys
 import sys
+from typing import NoReturn
 
 
 
 
 class NotifierException(Exception):
 class NotifierException(Exception):
@@ -20,13 +25,13 @@ class Notifier:
     # messages instead of writing them to the console.  This is
     # messages instead of writing them to the console.  This is
     # particularly useful for integrating the Python notify system
     # particularly useful for integrating the Python notify system
     # with the C++ notify system.
     # with the C++ notify system.
-    streamWriter = None
+    streamWriter: StreamWriter | None = None
     if ConfigVariableBool('notify-integrate', True):
     if ConfigVariableBool('notify-integrate', True):
         streamWriter = StreamWriter(Notify.out(), False)
         streamWriter = StreamWriter(Notify.out(), False)
 
 
     showTime = ConfigVariableBool('notify-timestamp', False)
     showTime = ConfigVariableBool('notify-timestamp', False)
 
 
-    def __init__(self, name, logger=None):
+    def __init__(self, name: str, logger: Logger | None = None) -> None:
         """
         """
         Parameters:
         Parameters:
             name (str): a string name given to this Notifier instance.
             name (str): a string name given to this Notifier instance.
@@ -42,12 +47,12 @@ class Notifier:
             self.__logger = logger
             self.__logger = logger
 
 
         # Global default levels are initialized here
         # Global default levels are initialized here
-        self.__info = 1
-        self.__warning = 1
-        self.__debug = 0
-        self.__logging = 0
+        self.__info = True
+        self.__warning = True
+        self.__debug = False
+        self.__logging = False
 
 
-    def setServerDelta(self, delta, timezone):
+    def setServerDelta(self, delta: float, timezone: int) -> None:
         """
         """
         Call this method on any Notify object to globally change the
         Call this method on any Notify object to globally change the
         timestamp printed for each line of all Notify objects.
         timestamp printed for each line of all Notify objects.
@@ -65,7 +70,7 @@ class Notifier:
 
 
         self.info("Notify clock adjusted by %s (and timezone adjusted by %s hours) to synchronize with server." % (PythonUtil.formatElapsedSeconds(delta), (time.timezone - timezone) / 3600))
         self.info("Notify clock adjusted by %s (and timezone adjusted by %s hours) to synchronize with server." % (PythonUtil.formatElapsedSeconds(delta), (time.timezone - timezone) / 3600))
 
 
-    def getTime(self):
+    def getTime(self) -> str:
         """
         """
         Return the time as a string suitable for printing at the
         Return the time as a string suitable for printing at the
         head of any notify message
         head of any notify message
@@ -74,14 +79,14 @@ class Notifier:
         # the task is out of focus on win32.  time.clock doesn't have this problem.
         # the task is out of focus on win32.  time.clock doesn't have this problem.
         return time.strftime(":%m-%d-%Y %H:%M:%S ", time.localtime(time.time() + self.serverDelta))
         return time.strftime(":%m-%d-%Y %H:%M:%S ", time.localtime(time.time() + self.serverDelta))
 
 
-    def getOnlyTime(self):
+    def getOnlyTime(self) -> str:
         """
         """
         Return the time as a string.
         Return the time as a string.
         The Only in the name is referring to not showing the date.
         The Only in the name is referring to not showing the date.
         """
         """
         return time.strftime("%H:%M:%S", time.localtime(time.time() + self.serverDelta))
         return time.strftime("%H:%M:%S", time.localtime(time.time() + self.serverDelta))
 
 
-    def __str__(self):
+    def __str__(self) -> str:
         """
         """
         Print handling routine
         Print handling routine
         """
         """
@@ -89,26 +94,26 @@ class Notifier:
                (self.__name, self.__info, self.__warning, self.__debug, self.__logging)
                (self.__name, self.__info, self.__warning, self.__debug, self.__logging)
 
 
     # Severity funcs
     # Severity funcs
-    def setSeverity(self, severity):
+    def setSeverity(self, severity: int) -> None:
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         if severity >= NSError:
         if severity >= NSError:
-            self.setWarning(0)
-            self.setInfo(0)
-            self.setDebug(0)
+            self.setWarning(False)
+            self.setInfo(False)
+            self.setDebug(False)
         elif severity == NSWarning:
         elif severity == NSWarning:
-            self.setWarning(1)
-            self.setInfo(0)
-            self.setDebug(0)
+            self.setWarning(True)
+            self.setInfo(False)
+            self.setDebug(False)
         elif severity == NSInfo:
         elif severity == NSInfo:
-            self.setWarning(1)
-            self.setInfo(1)
-            self.setDebug(0)
+            self.setWarning(True)
+            self.setInfo(True)
+            self.setDebug(False)
         elif severity <= NSDebug:
         elif severity <= NSDebug:
-            self.setWarning(1)
-            self.setInfo(1)
-            self.setDebug(1)
+            self.setWarning(True)
+            self.setInfo(True)
+            self.setDebug(True)
 
 
-    def getSeverity(self):
+    def getSeverity(self) -> int:
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         from panda3d.core import NSDebug, NSInfo, NSWarning, NSError
         if self.getDebug():
         if self.getDebug():
             return NSDebug
             return NSDebug
@@ -120,7 +125,7 @@ class Notifier:
             return NSError
             return NSError
 
 
     # error funcs
     # error funcs
-    def error(self, errorString, exception=NotifierException):
+    def error(self, errorString: object, exception: type[Exception] = NotifierException) -> NoReturn:
         """
         """
         Raise an exception with given string and optional type:
         Raise an exception with given string and optional type:
         Exception: error
         Exception: error
@@ -134,7 +139,7 @@ class Notifier:
         raise exception(errorString)
         raise exception(errorString)
 
 
     # warning funcs
     # warning funcs
-    def warning(self, warningString):
+    def warning(self, warningString: object) -> int:
         """
         """
         Issue the warning message if warn flag is on
         Issue the warning message if warn flag is on
         """
         """
@@ -148,20 +153,20 @@ class Notifier:
             self.__print(string)
             self.__print(string)
         return 1 # to allow assert myNotify.warning("blah")
         return 1 # to allow assert myNotify.warning("blah")
 
 
-    def setWarning(self, enable):
+    def setWarning(self, enable: bool) -> None:
         """
         """
         Enable/Disable the printing of warning messages
         Enable/Disable the printing of warning messages
         """
         """
         self.__warning = enable
         self.__warning = enable
 
 
-    def getWarning(self):
+    def getWarning(self) -> bool:
         """
         """
         Return whether the printing of warning messages is on or off
         Return whether the printing of warning messages is on or off
         """
         """
         return self.__warning
         return self.__warning
 
 
     # debug funcs
     # debug funcs
-    def debug(self, debugString):
+    def debug(self, debugString: object) -> int:
         """
         """
         Issue the debug message if debug flag is on
         Issue the debug message if debug flag is on
         """
         """
@@ -175,20 +180,20 @@ class Notifier:
             self.__print(string)
             self.__print(string)
         return 1 # to allow assert myNotify.debug("blah")
         return 1 # to allow assert myNotify.debug("blah")
 
 
-    def setDebug(self, enable):
+    def setDebug(self, enable: bool) -> None:
         """
         """
         Enable/Disable the printing of debug messages
         Enable/Disable the printing of debug messages
         """
         """
         self.__debug = enable
         self.__debug = enable
 
 
-    def getDebug(self):
+    def getDebug(self) -> bool:
         """
         """
         Return whether the printing of debug messages is on or off
         Return whether the printing of debug messages is on or off
         """
         """
         return self.__debug
         return self.__debug
 
 
     # info funcs
     # info funcs
-    def info(self, infoString):
+    def info(self, infoString: object) -> int:
         """
         """
         Print the given informational string, if info flag is on
         Print the given informational string, if info flag is on
         """
         """
@@ -202,39 +207,39 @@ class Notifier:
             self.__print(string)
             self.__print(string)
         return 1 # to allow assert myNotify.info("blah")
         return 1 # to allow assert myNotify.info("blah")
 
 
-    def getInfo(self):
+    def getInfo(self) -> bool:
         """
         """
         Return whether the printing of info messages is on or off
         Return whether the printing of info messages is on or off
         """
         """
         return self.__info
         return self.__info
 
 
-    def setInfo(self, enable):
+    def setInfo(self, enable: bool) -> None:
         """
         """
         Enable/Disable informational message  printing
         Enable/Disable informational message  printing
         """
         """
         self.__info = enable
         self.__info = enable
 
 
     # log funcs
     # log funcs
-    def __log(self, logEntry):
+    def __log(self, logEntry: str) -> None:
         """
         """
         Determine whether to send informational message to the logger
         Determine whether to send informational message to the logger
         """
         """
         if self.__logging:
         if self.__logging:
             self.__logger.log(logEntry)
             self.__logger.log(logEntry)
 
 
-    def getLogging(self):
+    def getLogging(self) -> bool:
         """
         """
         Return 1 if logging enabled, 0 otherwise
         Return 1 if logging enabled, 0 otherwise
         """
         """
         return self.__logging
         return self.__logging
 
 
-    def setLogging(self, enable):
+    def setLogging(self, enable: bool) -> None:
         """
         """
         Set the logging flag to int (1=on, 0=off)
         Set the logging flag to int (1=on, 0=off)
         """
         """
         self.__logging = enable
         self.__logging = enable
 
 
-    def __print(self, string):
+    def __print(self, string: str) -> None:
         """
         """
         Prints the string to output followed by a newline.
         Prints the string to output followed by a newline.
         """
         """
@@ -285,7 +290,7 @@ class Notifier:
             self.__print(string)
             self.__print(string)
         return 1 # to allow assert self.notify.debugStateCall(self)
         return 1 # to allow assert self.notify.debugStateCall(self)
 
 
-    def debugCall(self, debugString=''):
+    def debugCall(self, debugString: object = '') -> int:
         """
         """
         If this notify is in debug mode, print the time of the
         If this notify is in debug mode, print the time of the
         call followed by the notifier category and
         call followed by the notifier category and

+ 29 - 19
direct/src/directnotify/RotatingLog.py

@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import os
 import os
 import time
 import time
+from typing import Iterable
 
 
 
 
 class RotatingLog:
 class RotatingLog:
@@ -8,7 +11,12 @@ class RotatingLog:
     to a new file if the prior file is too large or after a time interval.
     to a new file if the prior file is too large or after a time interval.
     """
     """
 
 
-    def __init__(self, path="./log_file", hourInterval=24, megabyteLimit=1024):
+    def __init__(
+        self,
+        path: str = "./log_file",
+        hourInterval: int | None = 24,
+        megabyteLimit: int | None = 1024,
+    ) -> None:
         """
         """
         Args:
         Args:
             path: a full or partial path with file name.
             path: a full or partial path with file name.
@@ -28,33 +36,33 @@ class RotatingLog:
         if megabyteLimit is not None:
         if megabyteLimit is not None:
             self.sizeLimit = megabyteLimit*1024*1024
             self.sizeLimit = megabyteLimit*1024*1024
 
 
-    def __del__(self):
+    def __del__(self) -> None:
         self.close()
         self.close()
 
 
-    def close(self):
+    def close(self) -> None:
         if hasattr(self, "file"):
         if hasattr(self, "file"):
             self.file.flush()
             self.file.flush()
             self.file.close()
             self.file.close()
             self.closed = self.file.closed
             self.closed = self.file.closed
             del self.file
             del self.file
         else:
         else:
-            self.closed = 1
+            self.closed = True
 
 
-    def shouldRotate(self):
+    def shouldRotate(self) -> bool:
         """
         """
         Returns a bool about whether a new log file should
         Returns a bool about whether a new log file should
         be created and written to (while at the same time
         be created and written to (while at the same time
         stopping output to the old log file and closing it).
         stopping output to the old log file and closing it).
         """
         """
         if not hasattr(self, "file"):
         if not hasattr(self, "file"):
-            return 1
+            return True
         if self.timeLimit is not None and time.time() > self.timeLimit:
         if self.timeLimit is not None and time.time() > self.timeLimit:
-            return 1
+            return True
         if self.sizeLimit is not None and self.file.tell() > self.sizeLimit:
         if self.sizeLimit is not None and self.file.tell() > self.sizeLimit:
-            return 1
-        return 0
+            return True
+        return False
 
 
-    def filePath(self):
+    def filePath(self) -> str:
         dateString = time.strftime("%Y_%m_%d_%H", time.localtime())
         dateString = time.strftime("%Y_%m_%d_%H", time.localtime())
         for i in range(26):
         for i in range(26):
             limit = self.sizeLimit
             limit = self.sizeLimit
@@ -65,7 +73,7 @@ class RotatingLog:
         # Maybe we should clear the self.sizeLimit here... maybe.
         # Maybe we should clear the self.sizeLimit here... maybe.
         return path
         return path
 
 
-    def rotate(self):
+    def rotate(self) -> None:
         """
         """
         Rotate the log now.  You normally shouldn't need to call this.
         Rotate the log now.  You normally shouldn't need to call this.
         See write().
         See write().
@@ -88,12 +96,13 @@ class RotatingLog:
             #self.newlines = self.file.newlines # Python 2.3, maybe
             #self.newlines = self.file.newlines # Python 2.3, maybe
 
 
             if self.timeLimit is not None and time.time() > self.timeLimit:
             if self.timeLimit is not None and time.time() > self.timeLimit:
+                assert self.timeInterval is not None
                 self.timeLimit=time.time()+self.timeInterval
                 self.timeLimit=time.time()+self.timeInterval
         else:
         else:
             # We'll keep writing to the old file, if available.
             # We'll keep writing to the old file, if available.
             print("RotatingLog error: Unable to open new log file \"%s\"." % (path,))
             print("RotatingLog error: Unable to open new log file \"%s\"." % (path,))
 
 
-    def write(self, data):
+    def write(self, data: str) -> int | None:
         """
         """
         Write the data to either the current log or a new one,
         Write the data to either the current log or a new one,
         depending on the return of shouldRotate() and whether
         depending on the return of shouldRotate() and whether
@@ -105,14 +114,15 @@ class RotatingLog:
             r = self.file.write(data)
             r = self.file.write(data)
             self.file.flush()
             self.file.flush()
             return r
             return r
+        return None
 
 
-    def flush(self):
+    def flush(self) -> None:
         return self.file.flush()
         return self.file.flush()
 
 
-    def fileno(self):
+    def fileno(self) -> int:
         return self.file.fileno()
         return self.file.fileno()
 
 
-    def isatty(self):
+    def isatty(self) -> bool:
         return self.file.isatty()
         return self.file.isatty()
 
 
     def __next__(self):
     def __next__(self):
@@ -131,14 +141,14 @@ class RotatingLog:
     def xreadlines(self):
     def xreadlines(self):
         return self.file.xreadlines()
         return self.file.xreadlines()
 
 
-    def seek(self, offset, whence=0):
+    def seek(self, offset: int, whence: int = 0) -> int:
         return self.file.seek(offset, whence)
         return self.file.seek(offset, whence)
 
 
-    def tell(self):
+    def tell(self) -> int:
         return self.file.tell()
         return self.file.tell()
 
 
-    def truncate(self, size):
+    def truncate(self, size: int | None) -> int:
         return self.file.truncate(size)
         return self.file.truncate(size)
 
 
-    def writelines(self, sequence):
+    def writelines(self, sequence: Iterable[str]) -> None:
         return self.file.writelines(sequence)
         return self.file.writelines(sequence)

+ 1 - 1
direct/src/distributed/DistributedCartesianGrid.py

@@ -23,7 +23,7 @@ GRID_Z_OFFSET = 0.0
 
 
 class DistributedCartesianGrid(DistributedNode, CartesianGridBase):
 class DistributedCartesianGrid(DistributedNode, CartesianGridBase):
     notify = directNotify.newCategory("DistributedCartesianGrid")
     notify = directNotify.newCategory("DistributedCartesianGrid")
-    notify.setDebug(0)
+    notify.setDebug(False)
 
 
     VisualizeGrid = ConfigVariableBool("visualize-cartesian-grid", False)
     VisualizeGrid = ConfigVariableBool("visualize-cartesian-grid", False)
 
 

+ 57 - 0
tests/directnotify/test_DirectNotify.py

@@ -0,0 +1,57 @@
+import pytest
+from panda3d import core
+from direct.directnotify import DirectNotify, Logger, Notifier
+
+CATEGORY_NAME = 'test'
+
+
[email protected]
+def notify():
+    notify = DirectNotify.DirectNotify()
+    notify.newCategory(CATEGORY_NAME)
+    return notify
+
+
+def test_categories():
+    notify = DirectNotify.DirectNotify()
+    assert len(notify.getCategories()) == 0
+    assert notify.getCategory(CATEGORY_NAME) is None
+    notifier = notify.newCategory(CATEGORY_NAME, logger=Logger.Logger())
+    assert isinstance(notifier, Notifier.Notifier)
+    assert notify.getCategories() == [CATEGORY_NAME]
+
+
+def test_setDconfigLevels(notify):
+    config = core.ConfigVariableString('notify-level-' + CATEGORY_NAME, '')
+    notifier = notify.getCategory(CATEGORY_NAME)
+    config.value = 'error'
+    notify.setDconfigLevels()
+    assert notifier.getSeverity() == core.NS_error
+    config.value = 'warning'
+    notify.setDconfigLevels()
+    assert notifier.getSeverity() == core.NS_warning
+    config.value = 'info'
+    notify.setDconfigLevels()
+    assert notifier.getSeverity() == core.NS_info
+    config.value = 'debug'
+    notify.setDconfigLevels()
+    assert notifier.getSeverity() == core.NS_debug
+
+
+def test_setVerbose(notify):
+    notifier = notify.getCategory(CATEGORY_NAME)
+    notifier.setWarning(False)
+    notifier.setInfo(False)
+    notifier.setDebug(False)
+    notify.setVerbose()
+    assert notifier.getWarning()
+    assert notifier.getInfo()
+    assert notifier.getDebug()
+
+
+def test_giveNotify(notify):
+    class HasNotify:
+        notify = None
+
+    notify.giveNotify(HasNotify)
+    assert isinstance(HasNotify.notify, Notifier.Notifier)

+ 21 - 0
tests/directnotify/test_Logger.py

@@ -0,0 +1,21 @@
+import re
+from direct.directnotify import Logger
+
+LOG_TEXT = 'Arbitrary log text'
+
+
+def test_logging(tmp_path):
+    log_filename = str(tmp_path / 'log')
+    logger = Logger.Logger(log_filename)
+
+    assert logger.getTimeStamp()
+    logger.log(LOG_TEXT)
+    logger.setTimeStamp(False)
+    assert not logger.getTimeStamp()
+    logger.log(LOG_TEXT)
+    logger._Logger__closeLogFile()
+
+    log_file, = tmp_path.iterdir()
+    log_text = log_file.read_text()
+    pattern = rf'\d\d:\d\d:\d\d:\d\d: {LOG_TEXT}\n{LOG_TEXT}\n'
+    assert re.match(pattern, log_text)

+ 87 - 0
tests/directnotify/test_Notifier.py

@@ -0,0 +1,87 @@
+import io
+import re
+import time
+import pytest
+from panda3d import core
+from direct.directnotify import Logger, Notifier
+
+NOTIFIER_NAME = 'Test notifier'
+DEBUG_LOG = 'Debug log'
+INFO_LOG = 'Info log'
+WARNING_LOG = 'Warning log'
+ERROR_LOG = 'Error log'
+
+
[email protected]
+def log_io():
+    return io.StringIO()
+
+
[email protected]
+def notifier(log_io):
+    logger = Logger.Logger()
+    logger.setTimeStamp(False)
+    logger._Logger__logFile = log_io
+    notifier = Notifier.Notifier(NOTIFIER_NAME, logger)
+    notifier.setLogging(True)
+    return notifier
+
+
+def test_setServerDelta():
+    notifier = Notifier.Notifier(NOTIFIER_NAME)
+    notifier.setServerDelta(4.2, time.timezone)
+    assert Notifier.Notifier.serverDelta == 4
+    Notifier.Notifier.serverDelta = 0
+
+
+def test_logging(notifier, log_io):
+    notifier.setLogging(False)
+    assert not notifier.getLogging()
+    notifier.info(INFO_LOG)
+    assert log_io.getvalue() == ''
+
+    notifier.setLogging(True)
+    assert notifier.getLogging()
+    notifier.info(INFO_LOG)
+    assert log_io.getvalue() == f':{NOTIFIER_NAME}: {INFO_LOG}\n'
+
+
[email protected]('severity', (core.NS_debug, core.NS_info, core.NS_warning, core.NS_error))
+def test_severity(severity, notifier, log_io):
+    notifier.setSeverity(severity)
+    assert notifier.getSeverity() == severity
+
+    with pytest.raises(Notifier.NotifierException):
+        notifier.error(ERROR_LOG)
+    warning_return = notifier.warning(WARNING_LOG)
+    info_return = notifier.info(INFO_LOG)
+    debug_return = notifier.debug(DEBUG_LOG)
+    assert warning_return and info_return and debug_return
+
+    expected_logs = [
+        f'{Notifier.NotifierException}: {NOTIFIER_NAME}(error): {ERROR_LOG}',
+        f':{NOTIFIER_NAME}(warning): {WARNING_LOG}',
+        f':{NOTIFIER_NAME}: {INFO_LOG}',
+        f':{NOTIFIER_NAME}(debug): {DEBUG_LOG}',
+    ]
+    del expected_logs[6-severity:]
+    assert log_io.getvalue() == '\n'.join(expected_logs) + '\n'
+
+
+def test_custom_exception(notifier):
+    class CustomException(Exception):
+        pass
+
+    with pytest.raises(CustomException):
+        notifier.error(ERROR_LOG, CustomException)
+
+
+def test_debugCall(notifier, log_io):
+    notifier.setDebug(False)
+    return_value = notifier.debugCall(DEBUG_LOG)
+    assert return_value
+    assert log_io.getvalue() == ''
+    notifier.setDebug(True)
+    notifier.debugCall(DEBUG_LOG)
+    pattern = rf':\d\d:\d\d:\d\d:{NOTIFIER_NAME} "{DEBUG_LOG}" test_debugCall\(.*\)\n'
+    assert re.match(pattern, log_io.getvalue())

+ 45 - 0
tests/directnotify/test_RotatingLog.py

@@ -0,0 +1,45 @@
+import pytest
+from direct.directnotify import RotatingLog
+
+LOG_TEXT = 'Arbitrary log text'
+
+
[email protected]
+def log_dir(tmp_path):
+    log_dir = tmp_path / 'logs'
+    log_dir.mkdir()
+    return log_dir
+
+
[email protected]
+def rotating_log(log_dir):
+    log_filename = str(log_dir / 'log')
+    rotating_log = RotatingLog.RotatingLog(log_filename)
+    yield rotating_log
+    rotating_log.close()
+
+
+def test_rotation(rotating_log, log_dir):
+    rotating_log.sizeLimit = -1
+    rotating_log.write('1')
+    rotating_log.write('2')
+    written = [f.read_text() for f in log_dir.iterdir()]
+    assert written == ['1', '2'] or written == ['2', '1']
+
+
+def test_wrapper_methods(rotating_log, log_dir):
+    rotating_log.write('')
+    log_file, = log_dir.iterdir()
+
+    assert rotating_log.fileno() == rotating_log.file.fileno()
+    assert rotating_log.isatty() == rotating_log.file.isatty()
+
+    rotating_log.writelines([LOG_TEXT] * 10)
+    assert not log_file.read_text()
+    rotating_log.flush()
+    assert log_file.read_text() == LOG_TEXT * 10
+
+    assert rotating_log.tell() == len(LOG_TEXT) * 10
+    rotating_log.seek(len(LOG_TEXT))
+    rotating_log.truncate(None)
+    assert log_file.read_text() == LOG_TEXT