Repository: pydicom/pydicom

Issue:
TestFileSet test failures on i686 architecture
**Describe the bug**

2 TestFileSet tests fail on i686 (32-bit x86) architecture with an OverflowError.
FAILED pydicom/tests/test_fileset.py::TestFileSet_Modify::test_write_file_id
FAILED pydicom/tests/test_fileset.py::TestFileSet_Copy::test_file_id

**Expected behavior**

Test behavior should be consistent across architectures.

**Steps To Reproduce**

Run
```
pytest pydicom/tests/test_fileset.py 
```
in the pydicom directory on an i686 machine.
Virtualizing i686 using QEMU also reproduces the issue.

```
=================================== FAILURES ===================================
____________________ TestFileSet_Modify.test_write_file_id _____________________

self = <pydicom.tests.test_fileset.TestFileSet_Modify object at 0xe87f1430>
tiny = Dataset.file_meta -------------------------------
(0002, 0000) File Meta Information Group Length  UL: 200
(0002, 0001...Syntax UID in F UI: Explicit VR Little Endian
   (0020, 0013) Instance Number                     IS: '49'
   ---------

    def test_write_file_id(self, tiny):
        """Test that the File IDs character sets switch correctly."""
        tdir, ds = temporary_fs(tiny)
    
        def my_len(self):
            return 10**6 + 1
    
        FileSet.__len__ = my_len
        fs = FileSet(ds)
        assert 10**6 + 1 == len(fs)
        ds, paths = write_fs(fs)
        instance = fs._instances[-1]
        # Was written with alphanumeric File IDs
        assert "IM00001D" in instance.path
    
        def my_len(self):
            return 36**6 + 1
    
        FileSet.__len__ = my_len
        fs = FileSet(ds)
>       assert 36**6 + 1 == len(fs)
E       OverflowError: cannot fit 'int' into an index-sized integer

pydicom/tests/test_fileset.py:2249: OverflowError
________________________ TestFileSet_Copy.test_file_id _________________________

self = <pydicom.tests.test_fileset.TestFileSet_Copy object at 0xe837f370>
tiny = Dataset.file_meta -------------------------------
(0002, 0000) File Meta Information Group Length  UL: 200
(0002, 0001...Syntax UID in F UI: Explicit VR Little Endian
   (0020, 0013) Instance Number                     IS: '49'
   ---------
tdir = <TemporaryDirectory '/tmp/guix-build-python-pydicom-2.3.1.drv-0/tmpcki9joul'>

    def test_file_id(self, tiny, tdir):
        """Test that the File IDs character sets switch correctly."""
        def my_len(self):
            return 10**6 + 1
    
        FileSet.__len__ = my_len
        fs = FileSet(tiny)
        assert 10**6 + 1 == len(fs)
        fs, ds, paths = copy_fs(fs, tdir.name)
        instance = fs._instances[-1]
        # Was written with alphanumeric File IDs
        assert "IM00001D" in instance.path
    
        def my_len(self):
            return 36**6 + 1
    
        FileSet.__len__ = my_len
        fs = FileSet(tiny)
>       assert 36**6 + 1 == len(fs)
E       OverflowError: cannot fit 'int' into an index-sized integer

pydicom/tests/test_fileset.py:2580: OverflowError
```

**Your environment**

```
module       | version
------       | -------
platform     | Linux-6.0.7-i686-with-glibc2.33
Python       | 3.9.9 (main, Jan  1 1970, 00:00:01)  [GCC 10.3.0]
pydicom      | 2.3.1
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | 1.21.6
PIL          | 9.2.0
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_
```



Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#1864](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (a8d8648) into [main](https://app.codecov.io/gh/pydicom/pydicom/commit/34c68f910e9f20832a45a6f27e51ccad18320765?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (34c68f9) will **decrease** coverage by `0.01%`.
> Report is 1 commits behind head on main.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #1864      +/-   ##
==========================================
- Coverage   97.58%   97.57%   -0.01%     
==========================================
  Files          64       63       -1     
  Lines       10760    10739      -21     
==========================================
- Hits        10500    10479      -21     
  Misses        260      260              
```


| [Files Changed](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Coverage Δ | |
|---|---|---|
| [src/pydicom/dataelem.py](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-c3JjL3B5ZGljb20vZGF0YWVsZW0ucHk=) | `98.38% <ø> (-0.02%)` | :arrow_down: |
| [src/pydicom/pixel\_data\_handlers/rle\_handler.py](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-c3JjL3B5ZGljb20vcGl4ZWxfZGF0YV9oYW5kbGVycy9ybGVfaGFuZGxlci5weQ==) | `100.00% <ø> (ø)` | |
| [src/pydicom/fileset.py](https://app.codecov.io/gh/pydicom/pydicom/pull/1864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-c3JjL3B5ZGljb20vZmlsZXNldC5weQ==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e8b0eac77..41ade3499 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,13 +2,13 @@
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 - repo: https://github.com/crate-ci/typos
-  rev: typos-dict-v0.10.8
+  rev: v1.16.5
   hooks:
     - id: typos
       exclude: _.*_dict.py
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: v0.0.282
+  rev: v0.0.284
   hooks:
   - id: ruff
     args: [src, --fix, --exit-non-zero-on-fix]
diff --git a/build_tools/sphinx/github_link.py b/build_tools/sphinx/github_link.py
index bda79c540..51e640c34 100644
--- a/build_tools/sphinx/github_link.py
+++ b/build_tools/sphinx/github_link.py
@@ -40,7 +40,7 @@ def _linkcode_resolve(domain, info, package, url_fmt, revision):
         return
 
     class_name = info["fullname"].split(".")[0]
-    if type(class_name) != str:
+    if not isinstance(class_name, str):
         # Python 2 only
         class_name = class_name.encode("utf-8")
     module = __import__(info["module"], fromlist=[class_name])
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index 6ecf8d333..a35d02f6c 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -49,6 +49,11 @@ Changes
   :attr:`~pydicom.encoders.RLELosslessEncoder` instead.
 * Removed the `_storage_sopclass_uids` module, import UIDs from the `uid` module
   instead
+* The theoretical maximum number of instances supported by
+  :class:`~pydicom.fileset.FileSet` has been reduced to 1838265625 to ensure support
+  for 32-bit systems (:issue:`1743`)
+* The characters used by :func:`~pydicom.fileset.generate_filename` when
+  `alphanumeric` is ``True`` has been reduced to [0-9][A-I,K-Z]
 
 Enhancements
 ------------
diff --git a/examples/input_output/plot_read_rtplan.py b/examples/input_output/plot_read_rtplan.py
index 58f7aaaca..a135d529f 100644
--- a/examples/input_output/plot_read_rtplan.py
+++ b/examples/input_output/plot_read_rtplan.py
@@ -28,8 +28,8 @@ def list_beams(plan_dataset):
         cp0 = beam.ControlPointSequence[0]
         SSD = float(cp0.SourceToSurfaceDistance / 10)
         lines.append(
-            "{b.BeamName:^13s} {b.BeamNumber:8d} "
-            "{gantry:8.1f} {ssd:8.1f}".format(b=beam, gantry=cp0.GantryAngle, ssd=SSD)
+            f"{beam.BeamName:^13s} {beam.BeamNumber:8d} "
+            f"{cp0.GantryAngle:8.1f} {SSD:8.1f}"
         )
     return "\n".join(lines)
 
diff --git a/src/pydicom/encaps.py b/src/pydicom/encaps.py
index a5ed181c4..54b58dfd2 100644
--- a/src/pydicom/encaps.py
+++ b/src/pydicom/encaps.py
@@ -499,7 +499,7 @@ def read_item(fp: DicomFileLike) -> bytes | None:
     if length == 0xFFFFFFFF:
         raise ValueError(
             "Encapsulated data fragment had Undefined Length"
-            " at data position 0x{:x}".format(fp.tell() - 4)
+            f" at data position 0x{fp.tell() - 4:x}"
         )
 
     item_data = fp.read(length)
diff --git a/src/pydicom/fileset.py b/src/pydicom/fileset.py
index 761b73d7d..23c8219c4 100644
--- a/src/pydicom/fileset.py
+++ b/src/pydicom/fileset.py
@@ -1,6 +1,7 @@
 # Copyright 2008-2020 pydicom authors. See LICENSE file for details.
 """DICOM File-set handling."""
 
+from collections.abc import Iterator, Iterable, Callable
 import copy
 import os
 from pathlib import Path
@@ -8,7 +9,6 @@ import re
 import shutil
 from tempfile import TemporaryDirectory
 from typing import Optional, Union, Any, cast
-from collections.abc import Iterator, Iterable, Callable
 import warnings
 
 from pydicom.charset import default_encoding
@@ -80,7 +80,12 @@ def generate_filename(
     Maximum number of File IDs is:
 
     * Numeric: (10 ** (8 - `prefix`)) - `start`
-    * Alphanumeric: (36 ** (8 - `prefix`)) - `start`
+    * Alphanumeric: (35 ** (8 - `prefix`)) - `start`
+
+    .. versionchanged:: 3.0
+
+       The characters used when `alphanumeric` is ``True`` have been reduced to
+       [0-9][A-I,K-Z]
 
     Parameters
     ----------
@@ -91,7 +96,7 @@ def generate_filename(
         i.e. if you want to start at ``'00010'`` then `start` should be ``10``.
     alphanumeric : bool, optional
         If ``False`` (default) then only generate suffixes using the characters
-        [0-9], otherwise use [0-9][A-Z].
+        [0-9], otherwise use [0-9][A-I,K-Z].
 
     Yields
     ------
@@ -103,7 +108,7 @@ def generate_filename(
     if len(prefix) > 7:
         raise ValueError("The 'prefix' must be less than 8 characters long")
 
-    chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    chars = "0123456789ABCDEFGHIKLMNOPQRSTUVWXYZ"
     if not alphanumeric:
         chars = chars[:10]
 
@@ -245,7 +250,7 @@ class RecordNode(Iterable["RecordNode"]):
         if self.record_type == "PRIVATE":
             prefix = f"{prefix}{self.depth}"
 
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        chars = "0123456789ABCDEFGHIKLMNOPQRSTUVWXYZ"
         if not self.file_set._use_alphanumeric:
             chars = chars[:10]
 
@@ -1241,10 +1246,11 @@ class FileSet:
 
         if len(self) > 10**6:
             self._use_alphanumeric = True
-        if len(self) > 36**6:
+
+        if len(self) > 35**6:
             raise NotImplementedError(
                 "pydicom doesn't support writing File-sets with more than "
-                "2176782336 managed instances"
+                "1838265625 managed instances"
             )
 
         # Removals are detached from the tree
@@ -2080,10 +2086,11 @@ class FileSet:
         # Worst case scenario if all instances in one directory
         if len(self) > 10**6:
             self._use_alphanumeric = True
-        if len(self) > 36**6:
+
+        if len(self) > 35**6:
             raise NotImplementedError(
                 "pydicom doesn't support writing File-sets with more than "
-                "2176782336 managed instances"
+                "1838265625 managed instances"
             )
 
         # Remove the removals - must be first because the File IDs will be
diff --git a/src/pydicom/pixel_data_handlers/numpy_handler.py b/src/pydicom/pixel_data_handlers/numpy_handler.py
index 64a567d21..ef10b8f17 100644
--- a/src/pydicom/pixel_data_handlers/numpy_handler.py
+++ b/src/pydicom/pixel_data_handlers/numpy_handler.py
@@ -225,19 +225,17 @@ def get_pixeldata(ds: "Dataset", read_only: bool = False) -> "np.ndarray":
             )
         else:
             raise ValueError(
-                "The length of the pixel data in the dataset ({} bytes) "
-                "doesn't match the expected length ({} bytes). "
+                f"The length of the pixel data in the dataset ({actual_length} bytes) "
+                f"doesn't match the expected length ({padded_expected_len} bytes). "
                 "The dataset may be corrupted or there may be an issue "
-                "with the pixel data handler.".format(
-                    actual_length, padded_expected_len
-                )
+                "with the pixel data handler."
             )
     elif actual_length > padded_expected_len:
         # PS 3.5, Section 8.1.1
         msg = (
-            "The length of the pixel data in the dataset ({} bytes) indicates "
-            "it contains excess padding. {} bytes will be removed from the "
-            "end of the data".format(actual_length, actual_length - expected_len)
+            f"The length of the pixel data in the dataset ({actual_length} bytes) indicates "
+            f"it contains excess padding. {actual_length - expected_len} bytes will be removed from the "
+            "end of the data"
         )
         # PS 3.3, Annex C.7.6.3
         if ds.PhotometricInterpretation == "YBR_FULL_422":
@@ -248,10 +246,10 @@ def get_pixeldata(ds: "Dataset", read_only: bool = False) -> "np.ndarray":
                 msg = (
                     "The Photometric Interpretation of the dataset is "
                     "YBR_FULL_422, however the length of the pixel data "
-                    "({} bytes) is a third larger than expected ({} bytes) "
+                    f"({actual_length} bytes) is a third larger than expected ({expected_len} bytes) "
                     "which indicates that this may be incorrect. You may "
                     "need to change the Photometric Interpretation to "
-                    "the correct value.".format(actual_length, expected_len)
+                    "the correct value."
                 )
         warnings.warn(msg)
 
diff --git a/util/generate_cids/generate_concept_dicts.py b/util/generate_cids/generate_concept_dicts.py
index c1c990d0c..44ecd2059 100644
--- a/util/generate_cids/generate_concept_dicts.py
+++ b/util/generate_cids/generate_concept_dicts.py
@@ -311,7 +311,7 @@ if __name__ == "__main__":
                 except KeyError:
                     raise NotImplementedError(
                         "The DICOM scheme designator for the following FHIR system "
-                        "has not been specified: {}".format(system)
+                        f"has not been specified: {system}"
                     )
                 if scheme_designator not in concepts:
                     concepts[scheme_designator] = dict()


Test patch:
diff --git a/tests/test_fileset.py b/tests/test_fileset.py
index 332f77aa2..c45130112 100644
--- a/tests/test_fileset.py
+++ b/tests/test_fileset.py
@@ -440,7 +440,7 @@ class TestGenerateFilename:
         assert "00000008" == next(gen)
         assert "00000009" == next(gen)
         assert "0000000A" == next(gen)
-        for ii in range(24):
+        for ii in range(23):
             next(gen)
         assert "0000000Z" == next(gen)
         assert "00000010" == next(gen)
@@ -2242,17 +2242,17 @@ class TestFileSet_Modify:
         ds, paths = write_fs(fs)
         instance = fs._instances[-1]
         # Was written with alphanumeric File IDs
-        assert "IM00001D" in instance.path
+        assert "IM00001E" in instance.path
 
         def my_len(self):
-            return 36**6 + 1
+            return 35**6 + 1
 
         FileSet.__len__ = my_len
         fs = FileSet(ds)
-        assert 36**6 + 1 == len(fs)
+        assert 35**6 + 1 == len(fs)
         msg = (
             r"pydicom doesn't support writing File-sets with more than "
-            r"2176782336 managed instances"
+            r"1838265625 managed instances"
         )
         with pytest.raises(NotImplementedError, match=msg):
             fs.write()
@@ -2574,17 +2574,17 @@ class TestFileSet_Copy:
         fs, ds, paths = copy_fs(fs, tdir.name)
         instance = fs._instances[-1]
         # Was written with alphanumeric File IDs
-        assert "IM00001D" in instance.path
+        assert "IM00001E" in instance.path
 
         def my_len(self):
-            return 36**6 + 1
+            return 35**6 + 1
 
         FileSet.__len__ = my_len
         fs = FileSet(tiny)
-        assert 36**6 + 1 == len(fs)
+        assert 35**6 + 1 == len(fs)
         msg = (
             r"pydicom doesn't support writing File-sets with more than "
-            r"2176782336 managed instances"
+            r"1838265625 managed instances"
         )
         with pytest.raises(NotImplementedError, match=msg):
             fs.copy(tdir.name)
diff --git a/tests/test_json.py b/tests/test_json.py
index 9de3a27dc..0d369b5d8 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -462,12 +462,12 @@ class TestNumeric:
 
         ds_json = ds.to_json_dict()
 
-        assert type(ds_json["0009100B"]["Value"][0]) == int
-        assert type(ds_json["0009100C"]["Value"][0]) == int
-        assert type(ds_json["0009100D"]["Value"][0]) == int
-        assert type(ds_json["0009100E"]["Value"][0]) == int
-        assert type(ds_json["0009100F"]["Value"][0]) == float
-        assert type(ds_json["00091010"]["Value"][0]) == float
-        assert type(ds_json["00091014"]["Value"][0]) == int
-        assert type(ds_json["00091015"]["Value"][0]) == float
-        assert type(ds_json["00091102"]["Value"][0]) == int
+        assert isinstance(ds_json["0009100B"]["Value"][0], int)
+        assert isinstance(ds_json["0009100C"]["Value"][0], int)
+        assert isinstance(ds_json["0009100D"]["Value"][0], int)
+        assert isinstance(ds_json["0009100E"]["Value"][0], int)
+        assert isinstance(ds_json["0009100F"]["Value"][0], float)
+        assert isinstance(ds_json["00091010"]["Value"][0], float)
+        assert isinstance(ds_json["00091014"]["Value"][0], int)
+        assert isinstance(ds_json["00091015"]["Value"][0], float)
+        assert isinstance(ds_json["00091102"]["Value"][0], int)
diff --git a/tests/test_numpy_pixel_data.py b/tests/test_numpy_pixel_data.py
index 62b6ebfd3..1f93b8ade 100644
--- a/tests/test_numpy_pixel_data.py
+++ b/tests/test_numpy_pixel_data.py
@@ -1113,7 +1113,7 @@ class TestNumpy_GetPixelData:
             delattr(ds, attr)
             msg = (
                 r"Unable to convert the pixel data as the following required "
-                r"elements are missing from the dataset: {}".format(attr)
+                rf"elements are missing from the dataset: {attr}"
             )
             with pytest.raises(AttributeError, match=msg):
                 get_pixeldata(ds)
diff --git a/tests/test_rle_pixel_data.py b/tests/test_rle_pixel_data.py
index c3c8739f3..55c51c868 100644
--- a/tests/test_rle_pixel_data.py
+++ b/tests/test_rle_pixel_data.py
@@ -218,7 +218,7 @@ class TestNoNumpy_NoRLEHandler:
             ds.file_meta.TransferSyntaxUID = uid
             exc_msg = (
                 r"Unable to decode pixel data with a transfer syntax UID of "
-                r"'{}'".format(uid)
+                rf"'{uid}'"
             )
             with pytest.raises(NotImplementedError, match=exc_msg):
                 ds.pixel_array
@@ -264,7 +264,7 @@ class TestNoNumpy_RLEHandler:
             ds.file_meta.TransferSyntaxUID = uid
             exc_msg = (
                 r"Unable to decode pixel data with a transfer syntax UID of "
-                r"'{}'".format(uid)
+                rf"'{uid}'"
             )
             with pytest.raises(RuntimeError, match=exc_msg):
                 ds.pixel_array
@@ -323,7 +323,7 @@ class TestNumpy_NoRLEHandler:
             ds.file_meta.TransferSyntaxUID = uid
             exc_msg = (
                 r"Unable to decode pixel data with a transfer syntax UID of "
-                r"'{}'".format(uid)
+                rf"'{uid}'"
             )
             with pytest.raises(NotImplementedError, match=exc_msg):
                 ds.pixel_array
@@ -355,7 +355,7 @@ class TestNumpy_RLEHandler:
             ds.file_meta.TransferSyntaxUID = uid
             msg = (
                 r"Unable to decode pixel data with a transfer syntax UID of "
-                r"'{}'".format(uid)
+                rf"'{uid}'"
             )
             with pytest.raises(NotImplementedError, match=msg):
                 ds.pixel_array