Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pyscard for openSUSE:Factory 
checked in at 2025-01-15 17:44:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyscard (Old)
 and      /work/SRC/openSUSE:Factory/.python-pyscard.new.1881 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pyscard"

Wed Jan 15 17:44:05 2025 rev:22 rq:1237933 version:2.2.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pyscard/python-pyscard.changes    
2024-10-21 16:26:53.310682073 +0200
+++ /work/SRC/openSUSE:Factory/.python-pyscard.new.1881/python-pyscard.changes  
2025-01-15 17:44:08.303013652 +0100
@@ -1,0 +2,7 @@
+Tue Jan 14 19:57:53 UTC 2025 - Martin Hauke <mar...@gmx.de>
+
+- Update to version 2.2.1
+  * waitforcardevent(): do not miss events between 2 calls.
+  * Test, fix, and simplify ATR parsing.
+
+-------------------------------------------------------------------

Old:
----
  pyscard-2.2.0.tar.gz

New:
----
  pyscard-2.2.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pyscard.spec ++++++
--- /var/tmp/diff_new_pack.yWb4bf/_old  2025-01-15 17:44:09.323055886 +0100
+++ /var/tmp/diff_new_pack.yWb4bf/_new  2025-01-15 17:44:09.327056051 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pyscard
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2025 SUSE LLC
 # Copyright (c) 2011 LISA GmbH, Bingen, Germany.
 #
 # All modifications and additions to the file contributed by third parties
@@ -19,7 +19,7 @@
 
 %define modname pyscard
 Name:           python-pyscard
-Version:        2.2.0
+Version:        2.2.1
 Release:        0
 Summary:        Python module adding smart card support
 License:        LGPL-2.0-or-later

++++++ pyscard-2.2.0.tar.gz -> pyscard-2.2.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/ACKS new/pyscard-2.2.1/ACKS
--- old/pyscard-2.2.0/ACKS      2024-10-02 21:32:52.000000000 +0200
+++ new/pyscard-2.2.1/ACKS      2024-11-18 14:18:38.000000000 +0100
@@ -11,12 +11,29 @@
 Antonio Aranda
 Frank Aune
 Michel Beziat
+Peter Bittner
+Mark Bokil
 Mattias Brändström
+Dave Cahill
+Eduardo Castellanos
+Norman Denayer
+Stefan Droege
 Luc Duche
+Jonathan Giannuzzi
+Kevin Griffin
 Nodir Gulyamov
+Max Hausch
+Merrick Heley
 Yong David Huang
+Valdur Kana
+Kristiine
 Adam Laurie
+Kurt McKee
+Elouan Petereau
 Henryk Plotz
+Chris Post
 Michael Roehner
 Ludovic Rousseau
 David Wagner
+Harald Welte
+Alex Willmer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/ChangeLog new/pyscard-2.2.1/ChangeLog
--- old/pyscard-2.2.0/ChangeLog 2024-10-20 15:57:31.000000000 +0200
+++ new/pyscard-2.2.1/ChangeLog 2025-01-12 15:48:05.000000000 +0100
@@ -1,7 +1,16 @@
+2.2.1 (January 2025)
+====================
+- patches from Ludovic Rousseau
+   * waitforcardevent(): do not miss events between 2 calls
+   * Use Windows locale to decode Unicode text
+   * ACKS: add missing contributors
+- patches from Kurt McKee
+   * Test, fix, and simplify ATR parsing
+
 2.2.0 (October 2024)
 ====================
 - patches from Ludovic Rousseau
-   * PCSCCardRequest: 
+   * PCSCCardRequest:
     - handle KeyboardInterrupt in waitforcard() & waitforcardevent()
        - use a local PC/SC context to avoid locks
    * smartcard.util.padd(): do NOT modify the input parameter
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/PKG-INFO new/pyscard-2.2.1/PKG-INFO
--- old/pyscard-2.2.0/PKG-INFO  2024-10-20 16:07:11.435020200 +0200
+++ new/pyscard-2.2.1/PKG-INFO  2025-01-12 15:49:38.694440400 +0100
@@ -1,9 +1,9 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.2
 Name: pyscard
-Version: 2.2.0
+Version: 2.2.1
 Summary: Smartcard module for Python.
 Home-page: https://github.com/LudovicRousseau/pyscard
-Download-URL: 
https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.0/pyscard-2.2.0.tar.gz/download
+Download-URL: 
https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.1/pyscard-2.2.1.tar.gz/download
 Author: Ludovic Rousseau
 Author-email: ludovic.rouss...@free.fr
 Platform: linux
@@ -25,5 +25,16 @@
 Requires-Dist: typing_extensions; python_version == "3.9"
 Provides-Extra: gui
 Requires-Dist: wxPython; extra == "gui"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: download-url
+Dynamic: home-page
+Dynamic: platform
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
 
 Smartcard package for Python
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/setup.py new/pyscard-2.2.1/setup.py
--- old/pyscard-2.2.0/setup.py  2024-10-20 15:34:20.000000000 +0200
+++ new/pyscard-2.2.1/setup.py  2025-01-12 15:48:05.000000000 +0100
@@ -64,7 +64,7 @@
     except:
         platform_include_dirs = ["/usr/include/PCSC", 
"/usr/local/include/PCSC"]
 
-VERSION_INFO = (2, 2, 0, 0)
+VERSION_INFO = (2, 2, 1, 0)
 VERSION_STR = "%i.%i.%i" % VERSION_INFO[:3]
 VERSION_ALT = "%i,%01i,%01i,%04i" % VERSION_INFO
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/src/pyscard.egg-info/PKG-INFO 
new/pyscard-2.2.1/src/pyscard.egg-info/PKG-INFO
--- old/pyscard-2.2.0/src/pyscard.egg-info/PKG-INFO     2024-10-20 
16:07:11.000000000 +0200
+++ new/pyscard-2.2.1/src/pyscard.egg-info/PKG-INFO     2025-01-12 
15:49:38.000000000 +0100
@@ -1,9 +1,9 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.2
 Name: pyscard
-Version: 2.2.0
+Version: 2.2.1
 Summary: Smartcard module for Python.
 Home-page: https://github.com/LudovicRousseau/pyscard
-Download-URL: 
https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.0/pyscard-2.2.0.tar.gz/download
+Download-URL: 
https://sourceforge.net/projects/pyscard/files/pyscard/pyscard%202.2.1/pyscard-2.2.1.tar.gz/download
 Author: Ludovic Rousseau
 Author-email: ludovic.rouss...@free.fr
 Platform: linux
@@ -25,5 +25,16 @@
 Requires-Dist: typing_extensions; python_version == "3.9"
 Provides-Extra: gui
 Requires-Dist: wxPython; extra == "gui"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: download-url
+Dynamic: home-page
+Dynamic: platform
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
 
 Smartcard package for Python
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/ATR.py 
new/pyscard-2.2.1/src/smartcard/ATR.py
--- old/pyscard-2.2.0/src/smartcard/ATR.py      2024-10-20 15:28:52.000000000 
+0200
+++ new/pyscard-2.2.1/src/smartcard/ATR.py      2024-10-20 22:35:42.000000000 
+0200
@@ -22,14 +22,22 @@
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 """
 
+from __future__ import annotations
+
+import functools
+import operator
+import warnings
+
 from smartcard.Exceptions import SmartcardException
-from smartcard.util import toHexString
 
 
 class ATR:
-    """ATR class."""
+    """Parse and represent Answer to Reset sequences.
 
-    clockrateconversion = [
+    Answer to Reset sequences are defined in ISO 7816-3, section 8.
+    """
+
+    clockrateconversion: list[int | str] = [
         372,
         372,
         558,
@@ -47,7 +55,7 @@
         "RFU",
         "RFU",
     ]
-    bitratefactor = [
+    bitratefactor: list[int | str] = [
         "RFU",
         1,
         2,
@@ -65,21 +73,11 @@
         "RFU",
         "RFU",
     ]
-    currenttable = [25, 50, 100, "RFU"]
-
-    def __init__(self, atr):
-        """Construct a new atr from atr."""
-        self.atr = atr
-        self.__initInstance__()
+    currenttable: list[int | str] = [25, 50, 100, "RFU"]
 
-    def __checksyncbyte__(self):
-        """Check validity of TS."""
-        if not 0x3B == self.atr[0] and not 0x03F == self.atr[0]:
-            raise SmartcardException("invalid TS 0x%-0.2x" % self.atr[0])
+    def __init__(self, atr: list[int]) -> None:
+        """Parse ATR and initialize members:
 
-    def __initInstance__(self):
-        """
-        Parse ATR and initialize members:
           - TS: initial character
           - T0: format character
           - TA[n], TB[n], TC[n], TD[n], for n=0,1,...: protocol parameters
@@ -96,7 +94,13 @@
           - II: maximum programming current factor
           - N: extra guard time
         """
-        self.__checksyncbyte__()
+
+        if len(atr) < 2:
+            raise SmartcardException(f"ATR sequences must be at least 2 bytes 
long")
+        if atr[0] not in {0x3B, 0x3F}:
+            raise SmartcardException(f"invalid TS 0x{atr[0]:02x}")
+
+        self.atr = atr
 
         # initial character
         self.TS = self.atr[0]
@@ -108,244 +112,223 @@
         self.K = self.T0 & 0x0F
 
         # initialize optional characters lists
-        self.TA = []
-        self.TB = []
-        self.TC = []
-        self.TD = []
-        self.Y = []
-        self.hasTA = []
-        self.hasTB = []
-        self.hasTC = []
-        self.hasTD = []
-
-        TD = self.T0
-        hasTD = 1
-        n = 0
+        self.TA: list[None | int] = []
+        self.TB: list[None | int] = []
+        self.TC: list[None | int] = []
+        self.TD: list[None | int] = []
+        self.Y: list[int] = []
+
+        td: None | int = self.T0
         offset = 1
-        self.interfaceBytesCount = 0
-        while hasTD:
-            self.Y += [TD >> 4 & 0x0F]
-
-            self.hasTD += [(self.Y[n] & 0x08) != 0]
-            self.hasTC += [(self.Y[n] & 0x04) != 0]
-            self.hasTB += [(self.Y[n] & 0x02) != 0]
-            self.hasTA += [(self.Y[n] & 0x01) != 0]
+        while td is not None:
+            self.Y.append(td >> 4 & 0x0F)
 
             self.TA += [None]
             self.TB += [None]
             self.TC += [None]
             self.TD += [None]
 
-            if self.hasTA[n]:
-                self.TA[n] = self.atr[offset + self.hasTA[n]]
-            if self.hasTB[n]:
-                self.TB[n] = self.atr[offset + self.hasTA[n] + self.hasTB[n]]
-            if self.hasTC[n]:
-                self.TC[n] = self.atr[
-                    offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n]
-                ]
-            if self.hasTD[n]:
-                self.TD[n] = self.atr[
-                    offset
-                    + self.hasTA[n]
-                    + self.hasTB[n]
-                    + self.hasTC[n]
-                    + self.hasTD[n]
-                ]
-
-            self.interfaceBytesCount += (
-                self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + self.hasTD[n]
-            )
-            TD = self.TD[n]
-            hasTD = self.hasTD[n]
-            offset = (
-                offset + self.hasTA[n] + self.hasTB[n] + self.hasTC[n] + 
self.hasTD[n]
-            )
-            n = n + 1
+            if self.Y[-1] & 0x01:  # TA
+                offset += 1
+                self.TA[-1] = self.atr[offset]
+            if self.Y[-1] & 0x02:  # TB
+                offset += 1
+                self.TB[-1] = self.atr[offset]
+            if self.Y[-1] & 0x04:  # TC
+                offset += 1
+                self.TC[-1] = self.atr[offset]
+            if self.Y[-1] & 0x08:  # TD
+                offset += 1
+                self.TD[-1] = self.atr[offset]
+
+            td = self.TD[-1]
+
+        self.interfaceBytesCount = offset - 1
 
         # historical bytes
         self.historicalBytes = self.atr[offset + 1 : offset + 1 + self.K]
 
         # checksum
+        self.TCK: int | None = None
+        self.checksumOK: bool | None = None
         self.hasChecksum = len(self.atr) == offset + 1 + self.K + 1
         if self.hasChecksum:
             self.TCK = self.atr[-1]
-            checksum = 0
-            for b in self.atr[1:]:
-                checksum = checksum ^ b
-            self.checksumOK = checksum == 0
-        else:
-            self.TCK = None
+            self.checksumOK = functools.reduce(operator.xor, self.atr[1:]) == 0
 
         # clock-rate conversion factor
-        if self.hasTA[0]:
+        self.FI: int | None = None
+        if self.TA[0] is not None:
             self.FI = self.TA[0] >> 4 & 0x0F
-        else:
-            self.FI = None
 
         # bit-rate adjustment factor
-        if self.hasTA[0]:
+        self.DI: int | None = None
+        if self.TA[0] is not None:
             self.DI = self.TA[0] & 0x0F
-        else:
-            self.DI = None
 
         # maximum programming current factor
-        if self.hasTB[0]:
+        self.II: int | None = None
+        if self.TB[0] is not None:
             self.II = self.TB[0] >> 5 & 0x03
-        else:
-            self.II = None
 
         # programming voltage factor
-        if self.hasTB[0]:
+        self.PI1: int | None = None
+        if self.TB[0] is not None:
             self.PI1 = self.TB[0] & 0x1F
-        else:
-            self.PI1 = None
 
         # extra guard time
         self.N = self.TC[0]
 
-    def getChecksum(self):
+    @property
+    def hasTA(self) -> list[bool]:
+        """Deprecated. Replace usage with `ATR.TA[i] is not None`."""
+
+        warnings.warn("Replace usage with `ATR.TA[i] is not None`", 
DeprecationWarning)
+        return [ta is not None for ta in self.TA]
+
+    @property
+    def hasTB(self) -> list[bool]:
+        """Deprecated. Replace usage with `ATR.TB[i] is not None`."""
+
+        warnings.warn("Replace usage with `ATR.TB[i] is not None`", 
DeprecationWarning)
+        return [tb is not None for tb in self.TB]
+
+    @property
+    def hasTC(self) -> list[bool]:
+        """Deprecated. Replace usage with `ATR.TC[i] is not None`."""
+
+        warnings.warn("Replace usage with `ATR.TC[i] is not None`", 
DeprecationWarning)
+        return [tc is not None for tc in self.TC]
+
+    @property
+    def hasTD(self) -> list[bool]:
+        """Deprecated. Replace usage with `ATR.TD[i] is not None`."""
+
+        warnings.warn("Replace usage with `ATR.TD[i] is not None`", 
DeprecationWarning)
+        return [td is not None for td in self.TD]
+
+    def getChecksum(self) -> int | None:
         """Return the checksum of the ATR. Checksum is mandatory only
         for T=1."""
         return self.TCK
 
-    def getHistoricalBytes(self):
+    def getHistoricalBytes(self) -> list[int]:
         """Return historical bytes."""
         return self.historicalBytes
 
-    def getHistoricalBytesCount(self):
+    def getHistoricalBytesCount(self) -> int:
         """Return count of historical bytes."""
         return len(self.historicalBytes)
 
-    def getInterfaceBytesCount(self):
+    def getInterfaceBytesCount(self) -> int:
         """Return count of interface bytes."""
         return self.interfaceBytesCount
 
-    def getTA1(self):
+    def getTA1(self) -> int | None:
         """Return TA1 byte."""
         return self.TA[0]
 
-    def getTB1(self):
+    def getTB1(self) -> int | None:
         """Return TB1 byte."""
         return self.TB[0]
 
-    def getTC1(self):
+    def getTC1(self) -> int | None:
         """Return TC1 byte."""
         return self.TC[0]
 
-    def getTD1(self):
+    def getTD1(self) -> int | None:
         """Return TD1 byte."""
         return self.TD[0]
 
-    def getBitRateFactor(self):
+    def getBitRateFactor(self) -> int | str:
         """Return bit rate factor."""
         if self.DI is not None:
             return ATR.bitratefactor[self.DI]
-        else:
-            return 1
+        return 1
 
-    def getClockRateConversion(self):
+    def getClockRateConversion(self) -> int | str:
         """Return clock rate conversion."""
         if self.FI is not None:
             return ATR.clockrateconversion[self.FI]
-        else:
-            return 372
+        return 372
 
-    def getProgrammingCurrent(self):
+    def getProgrammingCurrent(self) -> int | str:
         """Return maximum programming current."""
         if self.II is not None:
             return ATR.currenttable[self.II]
-        else:
-            return 50
+        return 50
 
-    def getProgrammingVoltage(self):
+    def getProgrammingVoltage(self) -> int:
         """Return programming voltage."""
         if self.PI1 is not None:
             return 5 * (1 + self.PI1)
-        else:
-            return 5
+        return 5
 
-    def getGuardTime(self):
+    def getGuardTime(self) -> int | None:
         """Return extra guard time."""
         return self.N
 
-    def getSupportedProtocols(self):
+    def getSupportedProtocols(self) -> dict[str, bool]:
         """Returns a dictionary of supported protocols."""
-        protocols = {}
+        protocols: dict[str, bool] = {}
         for td in self.TD:
             if td is not None:
-                strprotocol = "T=%d" % (td & 0x0F)
-                protocols[strprotocol] = True
-        if not self.hasTD[0]:
+                protocols[f"T={td & 0x0F}"] = True
+        if self.TD[0] is None:
             protocols["T=0"] = True
         return protocols
 
-    def isT0Supported(self):
+    def isT0Supported(self) -> bool:
         """Return True if T=0 is supported."""
-        protocols = self.getSupportedProtocols()
-        return "T=0" in protocols
+        return "T=0" in self.getSupportedProtocols()
 
-    def isT1Supported(self):
+    def isT1Supported(self) -> bool:
         """Return True if T=1 is supported."""
-        protocols = self.getSupportedProtocols()
-        return "T=1" in protocols
+        return "T=1" in self.getSupportedProtocols()
 
-    def isT15Supported(self):
+    def isT15Supported(self) -> bool:
         """Return True if T=15 is supported."""
-        protocols = self.getSupportedProtocols()
-        return "T=15" in protocols
+        return "T=15" in self.getSupportedProtocols()
 
-    def dump(self):
-        """Dump the details of an ATR."""
+    def render(self) -> str:
+        """Render the ATR to a readable format."""
 
-        for i in range(0, len(self.TA)):
-            if self.TA[i] is not None:
-                print("TA%d: %x" % (i + 1, self.TA[i]))
-            if self.TB[i] is not None:
-                print("TB%d: %x" % (i + 1, self.TB[i]))
-            if self.TC[i] is not None:
-                print("TC%d: %x" % (i + 1, self.TC[i]))
-            if self.TD[i] is not None:
-                print("TD%d: %x" % (i + 1, self.TD[i]))
-
-        print("supported protocols " + ",".join(self.getSupportedProtocols()))
-        print("T=0 supported: " + str(self.isT0Supported()))
-        print("T=1 supported: " + str(self.isT1Supported()))
-
-        if self.getChecksum():
-            print("checksum: %d" % self.getChecksum())
-
-        print("\tclock rate conversion factor: " + 
str(self.getClockRateConversion()))
-        print("\tbit rate adjustment factor: " + str(self.getBitRateFactor()))
-
-        print("\tmaximum programming current: " + 
str(self.getProgrammingCurrent()))
-        print("\tprogramming voltage: " + str(self.getProgrammingVoltage()))
-
-        print("\tguard time: " + str(self.getGuardTime()))
-
-        print("nb of interface bytes: %d" % self.getInterfaceBytesCount())
-        print("nb of historical bytes: %d" % self.getHistoricalBytesCount())
-
-    def __str__(self):
-        """Returns a string representation of the ATR as a stream of bytes."""
-        return toHexString(self.atr)
-
-
-if __name__ == "__main__":
-    """Small sample illustrating the use of ATR."""
-
-    atrs = [
-        [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00],
-        [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00],
-        [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D],
-        [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03],
-        [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 
0x1B],
-        [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 
0x80, 0x0D],
-    ]
+        lines: list[str] = []
+        enumerated_tx_values = enumerate(zip(self.TA, self.TB, self.TC, 
self.TD), 1)
+        for i, (ta, tb, tc, td) in enumerated_tx_values:
+            if ta is not None:
+                lines.append(f"TA{i}: {ta:x}")
+            if tb is not None:
+                lines.append(f"TB{i}: {tb:x}")
+            if tc is not None:
+                lines.append(f"TC{i}: {tc:x}")
+            if td is not None:
+                lines.append(f"TD{i}: {td:x}")
+
+        lines.append(f"supported protocols 
{','.join(self.getSupportedProtocols())}")
+        lines.append(f"T=0 supported: {self.isT0Supported()}")
+        lines.append(f"T=1 supported: {self.isT1Supported()}")
+
+        if self.getChecksum() is not None:
+            lines.append(f"checksum: {self.getChecksum()}")
+
+        lines.append(f"\tclock rate conversion factor: 
{self.getClockRateConversion()}")
+        lines.append(f"\tbit rate adjustment factor: 
{self.getBitRateFactor()}")
+        lines.append(f"\tmaximum programming current: 
{self.getProgrammingCurrent()}")
+        lines.append(f"\tprogramming voltage: {self.getProgrammingVoltage()}")
+        lines.append(f"\tguard time: {self.getGuardTime()}")
+        lines.append(f"nb of interface bytes: {self.getInterfaceBytesCount()}")
+        lines.append(f"nb of historical bytes: 
{self.getHistoricalBytesCount()}")
+
+        return "\n".join(lines)
+
+    def dump(self) -> None:
+        """Deprecated. Replace usage with `print(ATR.render())`"""
+
+        warnings.warn("Replace usage with `print(ATR.render())`", 
DeprecationWarning)
+        print(self.render())
+
+    def __str__(self) -> str:
+        """Render the ATR as a space-separated string of uppercase hexadecimal 
pairs."""
 
-    for atr in atrs:
-        a = ATR(atr)
-        print(80 * "-")
-        print(a)
-        a.dump()
-        print(toHexString(a.getHistoricalBytes()))
+        return bytes(self.atr).hex(" ").upper()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/CardMonitoring.py 
new/pyscard-2.2.1/src/smartcard/CardMonitoring.py
--- old/pyscard-2.2.0/src/smartcard/CardMonitoring.py   2024-10-02 
21:32:52.000000000 +0200
+++ new/pyscard-2.2.1/src/smartcard/CardMonitoring.py   2025-01-07 
21:43:07.000000000 +0100
@@ -157,7 +157,7 @@
             """Runs until stopEvent is notified, and notify
             observers of all card insertion/removal.
             """
-            self.cardrequest = CardRequest(timeout=10)
+            self.cardrequest = CardRequest(timeout=60)
             while self.stopEvent.is_set() != 1:
                 try:
                     currentcards = self.cardrequest.waitforcardevent()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/pcsc/PCSCCardRequest.py 
new/pyscard-2.2.1/src/smartcard/pcsc/PCSCCardRequest.py
--- old/pyscard-2.2.0/src/smartcard/pcsc/PCSCCardRequest.py     2024-10-20 
15:29:45.000000000 +0200
+++ new/pyscard-2.2.1/src/smartcard/pcsc/PCSCCardRequest.py     2025-01-07 
21:43:07.000000000 +0100
@@ -302,7 +302,6 @@
         """Wait for card insertion or removal."""
         AbstractCardRequest.waitforcardevent(self)
         presentcards = []
-        readerstates = {}
 
         startDate = datetime.now()
         eventfound = False
@@ -310,6 +309,9 @@
         previous_readernames = self.getReaderNames()
         while not eventfound:
 
+            # get states from previous run
+            readerstates = self.readerstates
+
             # reinitialize at each iteration just in case a new reader appeared
             _readernames = self.getReaderNames()
             readernames = _readernames
@@ -318,35 +320,60 @@
                 # add PnP special reader
                 readernames.append("\\\\?PnP?\\Notification")
 
-            readerstates = {}
-            for reader in readernames:
-                # create a dictionary entry for new readers
-                readerstates[reader] = (reader, SCARD_STATE_UNAWARE)
-
-            hresult, newstates = SCardGetStatusChange(
-                self.hcontext, 0, list(readerstates.values())
-            )
+            # first call?
+            if len(readerstates) == 0:
+                # init
+                for reader in readernames:
+                    # create a dictionary entry for new readers
+                    readerstates[reader] = (reader, SCARD_STATE_UNAWARE)
+
+                hresult, newstates = SCardGetStatusChange(
+                    self.hcontext, 0, list(readerstates.values())
+                )
 
             # check if a new reader with a card has just been connected
-            for state in newstates:
-                readername, eventstate, _ = state
+            for reader in _readernames:
+                # is the reader a new one?
+                if reader not in readerstates:
+                    # create a dictionary entry for new reader
+                    readerstates[reader] = (reader, SCARD_STATE_UNAWARE)
+
+                    hresult, newstates = SCardGetStatusChange(
+                        self.hcontext, 0, list(readerstates.values())
+                    )
+
+                    # added reader is the last one (index is -1)
+                    _, state, _ = newstates[-1]
+                    if state & SCARD_STATE_PRESENT:
+                        eventfound = True
 
-                # the reader is a new one
-                if readername not in previous_readernames:
-                    if eventstate & SCARD_STATE_PRESENT:
+            # check if a reader has been removed
+            to_remove = []
+            for reader in readerstates:
+                if reader not in readernames:
+                    _, state = readerstates[reader]
+                    # was the card present?
+                    if state & SCARD_STATE_PRESENT:
                         eventfound = True
 
+                    to_remove.append(reader)
+
+            if to_remove:
+                for reader in to_remove:
+                    # remove reader
+                    del readerstates[reader]
+
+                # get newstates with new reader list
+                hresult, newstates = SCardGetStatusChange(
+                    self.hcontext, 0, list(readerstates.values())
+                )
+
             if eventfound:
                 break
 
-            # update previous readers list
+            # update previous readers list (without PnP special reader)
             previous_readernames = _readernames
 
-            # update readerstate
-            for state in newstates:
-                readername, eventstate, atr = state
-                readerstates[readername] = (readername, eventstate)
-
             # wait for card insertion
             self.readerstates = readerstates
             waitThread = threading.Thread(target=self.getStatusChange)
@@ -401,31 +428,17 @@
                 for state in newstates:
                     readername, eventstate, atr = state
 
-                    # ignore Pnp reader
+                    # ignore PnP reader
                     if readername == "\\\\?PnP?\\Notification":
                         continue
 
-                    _, oldstate = readerstates[readername]
-
-                    # the status can change on a card already inserted, e.g.
-                    # unpowered, in use, ... Clear the state changed bit if
-                    # the card was already inserted and is still inserted
-                    if (
-                        oldstate & SCARD_STATE_PRESENT
-                        and eventstate & (SCARD_STATE_CHANGED | 
SCARD_STATE_PRESENT)
-                        == SCARD_STATE_CHANGED | SCARD_STATE_PRESENT
-                    ):
-                        eventstate = eventstate & (0xFFFFFFFF ^ 
SCARD_STATE_CHANGED)
-
                     if eventstate & SCARD_STATE_CHANGED:
-                        if (
-                            # a reader or a card has been removed
-                            oldstate & SCARD_STATE_PRESENT
-                            or
-                            # a card has been inserted
-                            eventstate & SCARD_STATE_PRESENT
-                        ):
-                            eventfound = True
+                        eventfound = True
+
+            # update readerstates for next SCardGetStatusChange() call
+            self.readerstates = {}
+            for reader, state, atr in newstates:
+                self.readerstates[reader] = (reader, state)
 
         # return all the cards present
         for state in newstates:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/src/smartcard/scard/helpers.c 
new/pyscard-2.2.1/src/smartcard/scard/helpers.c
--- old/pyscard-2.2.0/src/smartcard/scard/helpers.c     2024-10-02 
21:32:52.000000000 +0200
+++ new/pyscard-2.2.1/src/smartcard/scard/helpers.c     2025-01-07 
21:43:07.000000000 +0100
@@ -309,7 +309,7 @@
     if( NULL!=source )
     {
 #if (PY_MAJOR_VERSION >= 3) && defined(WIN32)
-        pystr = PyUnicode_Decode( source, strlen(source), "cp1250" , NULL);
+        pystr = PyUnicode_DecodeLocale(source, NULL);
 #else
         pystr = PyString_FromString( source );
 #endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyscard-2.2.0/test/test_ATR.py 
new/pyscard-2.2.1/test/test_ATR.py
--- old/pyscard-2.2.0/test/test_ATR.py  2024-10-20 15:28:52.000000000 +0200
+++ new/pyscard-2.2.1/test/test_ATR.py  2024-10-20 22:35:42.000000000 +0200
@@ -1,3 +1,6 @@
+import re
+import textwrap
+
 import pytest
 
 from smartcard.ATR import ATR
@@ -7,142 +10,173 @@
 
 def test_atr1(capsys):
     atr = [0x3F, 0x65, 0x25, 0x00, 0x2C, 0x09, 0x69, 0x90, 0x00]
-    data_out = """TB1: 25
-TC1: 0
-supported protocols T=0
-T=0 supported: True
-T=1 supported: False
-\tclock rate conversion factor: 372
-\tbit rate adjustment factor: 1
-\tmaximum programming current: 50
-\tprogramming voltage: 30
-\tguard time: 0
-nb of interface bytes: 2
-nb of historical bytes: 5
-"""
+    data_out = textwrap.dedent(
+        """\
+        TB1: 25
+        TC1: 0
+        supported protocols T=0
+        T=0 supported: True
+        T=1 supported: False
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 50
+        \tprogramming voltage: 30
+        \tguard time: 0
+        nb of interface bytes: 2
+        nb of historical bytes: 5
+        """
+    )
     a = ATR(atr)
-    a.dump()
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
 def test_atr2(capsys):
     atr = [0x3F, 0x65, 0x25, 0x08, 0x93, 0x04, 0x6C, 0x90, 0x00]
-    data_out = """TB1: 25
-TC1: 8
-supported protocols T=0
-T=0 supported: True
-T=1 supported: False
-\tclock rate conversion factor: 372
-\tbit rate adjustment factor: 1
-\tmaximum programming current: 50
-\tprogramming voltage: 30
-\tguard time: 8
-nb of interface bytes: 2
-nb of historical bytes: 5
-"""
+    data_out = textwrap.dedent(
+        """\
+        TB1: 25
+        TC1: 8
+        supported protocols T=0
+        T=0 supported: True
+        T=1 supported: False
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 50
+        \tprogramming voltage: 30
+        \tguard time: 8
+        nb of interface bytes: 2
+        nb of historical bytes: 5
+        """
+    )
     a = ATR(atr)
-    a.dump()
-
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
 def test_atr3(capsys):
     atr = [0x3B, 0x16, 0x94, 0x7C, 0x03, 0x01, 0x00, 0x00, 0x0D]
-    data_out = """TA1: 94
-supported protocols T=0
-T=0 supported: True
-T=1 supported: False
-\tclock rate conversion factor: 512
-\tbit rate adjustment factor: 8
-\tmaximum programming current: 50
-\tprogramming voltage: 5
-\tguard time: None
-nb of interface bytes: 1
-nb of historical bytes: 6
-"""
+    data_out = textwrap.dedent(
+        """\
+        TA1: 94
+        supported protocols T=0
+        T=0 supported: True
+        T=1 supported: False
+        \tclock rate conversion factor: 512
+        \tbit rate adjustment factor: 8
+        \tmaximum programming current: 50
+        \tprogramming voltage: 5
+        \tguard time: None
+        nb of interface bytes: 1
+        nb of historical bytes: 6
+        """
+    )
     a = ATR(atr)
-    a.dump()
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
 def test_atr4(capsys):
     atr = [0x3B, 0x65, 0x00, 0x00, 0x9C, 0x11, 0x01, 0x01, 0x03]
-    data_out = """TB1: 0
-TC1: 0
-supported protocols T=0
-T=0 supported: True
-T=1 supported: False
-\tclock rate conversion factor: 372
-\tbit rate adjustment factor: 1
-\tmaximum programming current: 25
-\tprogramming voltage: 5
-\tguard time: 0
-nb of interface bytes: 2
-nb of historical bytes: 5
-"""
+    data_out = textwrap.dedent(
+        """\
+        TB1: 0
+        TC1: 0
+        supported protocols T=0
+        T=0 supported: True
+        T=1 supported: False
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 25
+        \tprogramming voltage: 5
+        \tguard time: 0
+        nb of interface bytes: 2
+        nb of historical bytes: 5
+        """
+    )
     a = ATR(atr)
-    a.dump()
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
 def test_atr5(capsys):
     atr = [0x3B, 0xE3, 0x00, 0xFF, 0x81, 0x31, 0x52, 0x45, 0xA1, 0xA2, 0xA3, 
0x1B]
-    data_out = """TB1: 0
-TC1: ff
-TD1: 81
-TD2: 31
-TA3: 52
-TB3: 45
-supported protocols T=1
-T=0 supported: False
-T=1 supported: True
-checksum: 27
-\tclock rate conversion factor: 372
-\tbit rate adjustment factor: 1
-\tmaximum programming current: 25
-\tprogramming voltage: 5
-\tguard time: 255
-nb of interface bytes: 6
-nb of historical bytes: 3
-"""
+    data_out = textwrap.dedent(
+        """\
+        TB1: 0
+        TC1: ff
+        TD1: 81
+        TD2: 31
+        TA3: 52
+        TB3: 45
+        supported protocols T=1
+        T=0 supported: False
+        T=1 supported: True
+        checksum: 27
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 25
+        \tprogramming voltage: 5
+        \tguard time: 255
+        nb of interface bytes: 6
+        nb of historical bytes: 3
+        """
+    )
     a = ATR(atr)
-    a.dump()
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
 def test_atr6(capsys):
     atr = [0x3B, 0xE5, 0x00, 0x00, 0x81, 0x21, 0x45, 0x9C, 0x10, 0x01, 0x00, 
0x80, 0x0D]
-    data_out = """TB1: 0
-TC1: 0
-TD1: 81
-TD2: 21
-TB3: 45
-supported protocols T=1
-T=0 supported: False
-T=1 supported: True
-checksum: 13
-\tclock rate conversion factor: 372
-\tbit rate adjustment factor: 1
-\tmaximum programming current: 25
-\tprogramming voltage: 5
-\tguard time: 0
-nb of interface bytes: 5
-nb of historical bytes: 5
-"""
-    a = ATR(atr)
-    a.dump()
+    data_out = textwrap.dedent(
+        """\
+        TB1: 0
+        TC1: 0
+        TD1: 81
+        TD2: 21
+        TB3: 45
+        supported protocols T=1
+        T=0 supported: False
+        T=1 supported: True
+        checksum: 13
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 25
+        \tprogramming voltage: 5
+        \tguard time: 0
+        nb of interface bytes: 5
+        nb of historical bytes: 5
+        """
+    )
+    a = ATR(atr)
+    with pytest.warns(DeprecationWarning, 
match=re.escape("print(ATR.render())")):
+        a.dump()
     stdout, _ = capsys.readouterr()
     assert stdout == data_out
 
 
-def test_atr_ts():
-    atr = [0x42]
-    with pytest.raises(SmartcardException):
+@pytest.mark.parametrize(
+    "ts",
+    (
+        pytest.param("0x42", id="numeric"),
+        pytest.param("0xaa", id="lowercase"),
+        pytest.param("0x00", id="zero padding"),
+    ),
+)
+def test_invalid_ts(ts: str):
+    atr = [int(ts[2:], 16), 0x00]
+    with pytest.raises(SmartcardException, match=f"invalid TS {ts}"):
         ATR(atr)
 
 
@@ -172,3 +206,210 @@
     """
 
     assert len(getattr(ATR, field)) == expected_length
+
+
+@pytest.mark.parametrize(
+    "atr,",
+    (
+        pytest.param([], id="ATR is too short (0 bytes)"),
+        pytest.param([0x3B], id="ATR is too short (1 byte, valid TS)"),
+    ),
+)
+def test_invalid_atr_lengths(atr: list[int]):
+    """Verify that short ATRs raise exceptions."""
+
+    with pytest.raises(SmartcardException, match="at least 2 bytes"):
+        ATR(atr)
+
+
+@pytest.mark.parametrize("ts", (0x3B, 0x3F))
+def test_2_bytes(ts):
+    """Verify that a completely empty ATR parses well."""
+
+    atr = ATR([ts, 0b0000_0000])
+    #                |||| `-- no historical bytes
+    #                |||`-- no TA
+    #                ||`-- no TB
+    #                |`-- no TC
+    #                `-- no TD
+    assert atr.getTA1() is None
+    assert atr.getTB1() is None
+    assert atr.II is None
+    assert atr.PI1 is None
+    assert atr.getTC1() is None
+    assert atr.getTD1() is None
+    assert atr.getChecksum() is None
+    assert atr.getGuardTime() is None
+    assert atr.getHistoricalBytesCount() == 0
+    assert atr.getHistoricalBytes() == []
+    assert atr.getInterfaceBytesCount() == 0
+
+    # Default values
+    assert atr.getBitRateFactor() == 1
+    assert atr.getClockRateConversion() == 372
+    assert atr.getProgrammingCurrent() == 50
+    assert atr.getProgrammingVoltage() == 5
+
+    # Protocols
+    assert len(atr.getSupportedProtocols()) == 1
+    assert "T=0" in atr.getSupportedProtocols()
+    assert atr.isT0Supported() is True
+    assert atr.isT1Supported() is False
+    assert atr.isT15Supported() is False
+
+    # Rendering
+    expected_rendering = textwrap.dedent(
+        """\
+        supported protocols T=0
+        T=0 supported: True
+        T=1 supported: False
+        \tclock rate conversion factor: 372
+        \tbit rate adjustment factor: 1
+        \tmaximum programming current: 50
+        \tprogramming voltage: 5
+        \tguard time: None
+        nb of interface bytes: 0
+        nb of historical bytes: 0
+        """.rstrip()
+    )
+    assert atr.render() == expected_rendering
+
+    # Warnings
+    with pytest.warns(DeprecationWarning, match="ATR.TA"):
+        assert atr.hasTA == [False]
+    with pytest.warns(DeprecationWarning, match="ATR.TB"):
+        assert atr.hasTB == [False]
+    with pytest.warns(DeprecationWarning, match="ATR.TC"):
+        assert atr.hasTC == [False]
+    with pytest.warns(DeprecationWarning, match="ATR.TD"):
+        assert atr.hasTD == [False]
+
+
+def test_only_ta1():
+    """Verify that TA1 can be conveyed standalone."""
+
+    atr = ATR([0x3B, 0b0001_0000, 0xA7])
+    #                     `-- only enable TA
+    assert atr.TA == [0xA7]
+    assert "TA1: a7\n" in atr.render()
+    with pytest.warns(DeprecationWarning, match="ATR.TA"):
+        assert atr.hasTA == [True]
+    # TA1 affects these values
+    assert atr.getClockRateConversion() == 768
+    assert atr.getBitRateFactor() == 64
+    # Sanity check
+    assert atr.TB == atr.TC == atr.TD == [None]
+    assert atr.N is None
+    assert atr.getInterfaceBytesCount() == 1
+    assert atr.getHistoricalBytesCount() == 0
+    assert atr.hasChecksum is False
+    assert atr.checksumOK is None
+    assert atr.getChecksum() is None
+
+
+def test_only_tb1():
+    """Verify that TB1 can be conveyed standalone.
+
+    TB1 and TB2 are deprecated in ISO 7816-3 2006, so no values are checked 
here.
+    """
+
+    atr = ATR([0x3B, 0b0010_0000, 0b0_10_11111])
+    #                    `-- only enable TB
+    assert atr.TB == [0b0_10_11111]
+    assert "TB1: 5f\n" in atr.render()
+    with pytest.warns(DeprecationWarning, match="ATR.TB"):
+        assert atr.hasTB == [True]
+    # TB1 affects these values
+    assert atr.II == 0b10
+    assert atr.PI1 == 0b11111
+    assert atr.getProgrammingVoltage() != 5
+    assert atr.getProgrammingCurrent() != 50
+    # Sanity check
+    assert atr.TA == atr.TC == atr.TD == [None]
+    assert atr.N is None
+    assert atr.getInterfaceBytesCount() == 1
+    assert atr.getHistoricalBytesCount() == 0
+    assert atr.hasChecksum is False
+    assert atr.checksumOK is None
+    assert atr.getChecksum() is None
+
+
+def test_only_tc1():
+    """Verify that TC1 can be conveyed standalone."""
+
+    atr = ATR([0x3B, 0b0100_0000, 0xC1])
+    #                   `-- only enable TC
+    assert atr.TC == [0xC1]
+    assert "TC1: c1\n" in atr.render()
+    with pytest.warns(DeprecationWarning, match="ATR.TC"):
+        assert atr.hasTC == [True]
+    # TC1 affects these values
+    assert atr.N == 0xC1
+    # Sanity check
+    assert atr.TA == atr.TB == atr.TD == [None]
+    assert atr.getInterfaceBytesCount() == 1
+    assert atr.getHistoricalBytesCount() == 0
+    assert atr.hasChecksum is False
+    assert atr.checksumOK is None
+    assert atr.getChecksum() is None
+
+
+def test_only_td1():
+    """Verify that TD1 can be conveyed standalone."""
+
+    atr = ATR([0x3B, 0b1000_0000, 0x00])
+    #                  `-- only enable TD
+    assert atr.TD == [0x00, None]
+    assert atr.isT0Supported() is True
+    assert atr.isT1Supported() is False
+    assert atr.isT15Supported() is False
+    assert "TD1: 0\n" in atr.render()
+    with pytest.warns(DeprecationWarning, match="ATR.TD"):
+        assert atr.hasTD == [True, False]
+    # Sanity check
+    assert atr.TA == atr.TB == atr.TC == [None, None]
+    assert atr.N is None
+    assert atr.getHistoricalBytesCount() == 0
+    assert atr.hasChecksum is False
+    assert atr.checksumOK is None
+    assert atr.getChecksum() is None
+
+
+def test_historical_bytes():
+    """Verify that historical bytes can be conveyed standalone."""
+
+    atr = ATR([0x3B, 0x0F, *list(range(15))])
+    #                   `-- indicate 15 historical bytes
+    assert atr.K == 15
+    assert atr.getHistoricalBytesCount() == 15
+    assert atr.getHistoricalBytes() == list(range(15))
+    # Sanity check
+    assert atr.TA == atr.TB == atr.TC == atr.TD == [None]
+    assert atr.N is None
+    assert atr.hasChecksum is False
+    assert atr.checksumOK is None
+    assert atr.getChecksum() is None
+
+
+@pytest.mark.parametrize("ts", (0x3B, 0x3F))
+@pytest.mark.parametrize("atr_bytes", ([0x00, 0x00], [0x1, 0xFE, 0xFF]))
+def test_valid_checksums(ts, atr_bytes):
+    """Verify behavior of valid checksums."""
+
+    atr = ATR([ts] + atr_bytes)
+    assert atr.hasChecksum is True
+    assert atr.checksumOK is True
+    assert atr.getChecksum() == atr_bytes[-1]
+    assert f"checksum: {atr_bytes[-1]}\n" in atr.render()
+
+
+@pytest.mark.parametrize("ts", (0x3B, 0x3F))
+@pytest.mark.parametrize("atr_bytes", ([0x00, 0x01], [0x01, 0xFE, 0x0]))
+def test_invalid_checksums(ts, atr_bytes):
+    """Verify behavior of invalid checksums."""
+
+    atr = ATR([ts] + atr_bytes)
+    assert atr.hasChecksum is True
+    assert atr.checksumOK is False
+    assert atr.getChecksum() == atr_bytes[-1]
+    assert f"checksum: {atr_bytes[-1]:x}\n" in atr.render()

Reply via email to