Files
dicom2pacs/dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/cli/main.py
René Mathieu 0fef8d96c5 Initial commit
2026-01-17 13:49:51 +01:00

233 lines
7.4 KiB
Python
Executable File

# Copyright 2020 pydicom authors. See LICENSE file for details.
"""Pydicom command line interface program
Each subcommand is a module within pydicom.cli, which
defines an add_subparser(subparsers) function to set argparse
attributes, and calls set_defaults(func=callback_function)
"""
import argparse
from importlib.metadata import entry_points
import re
import sys
from typing import cast, Any
from collections.abc import Callable
from pydicom import dcmread
from pydicom.data.data_manager import get_charset_files, get_testdata_file
from pydicom.dataset import Dataset
subparsers: argparse._SubParsersAction | None = None
# Restrict the allowed syntax tightly, since use Python `eval`
# on the expression. Do not allow callables, or assignment, for example.
re_kywd_or_item = (
r"\w+" # Keyword (\w allows underscore, needed for file_meta)
r"(\[(-)?\d+\])?" # Optional [index] or [-index]
)
re_file_spec_object = re.compile(re_kywd_or_item + r"(\." + re_kywd_or_item + r")*$")
filespec_help = (
"File specification, in format [pydicom::]filename[::element]. "
"If `pydicom::` prefix is present, then use the pydicom "
"test file with that name. If `element` is given, "
"use only that data element within the file. "
"Examples: "
"path/to/your_file.dcm, "
"your_file.dcm::StudyDate, "
"pydicom::rtplan.dcm::BeamSequence[0], "
"yourplan.dcm::BeamSequence[0].BeamNumber"
)
def eval_element(ds: Dataset, element: str) -> Any:
try:
return eval("ds." + element, {"ds": ds})
except AttributeError:
raise argparse.ArgumentTypeError(
f"Data element '{element}' is not in the dataset"
)
except IndexError as e:
raise argparse.ArgumentTypeError(f"'{element}' has an index error: {e}")
def filespec_parts(filespec: str) -> tuple[str, str, str]:
"""Parse the filespec format into prefix, filename, element
Format is [prefix::filename::element]
Note that ':' can also exist in valid filename, e.g. r'c:\temp\test.dcm'
"""
*prefix_file, last = filespec.split("::")
if not prefix_file: # then only the filename component
return "", last, ""
prefix = "pydicom" if prefix_file[0] == "pydicom" else ""
if prefix:
prefix_file.pop(0)
# If list empty after pop above, then have pydicom::filename
if not prefix_file:
return prefix, last, ""
return prefix, "".join(prefix_file), last
def filespec_parser(filespec: str) -> list[tuple[Dataset, Any]]:
"""Utility to return a dataset and an optional data element value within it
Note: this is used as an argparse 'type' for adding parsing arguments.
Parameters
----------
filespec: str
A filename with optional `pydicom::` prefix and optional data element,
in format:
[pydicom::]<filename>[::<element>]
If an element is specified, it must be a path to a data element,
sequence item (dataset), or a sequence.
Examples:
your_file.dcm
your_file.dcm::StudyDate
pydicom::rtplan.dcm::BeamSequence[0]
pydicom::rtplan.dcm::BeamSequence[0].BeamLimitingDeviceSequence
Returns
-------
List[Tuple[Dataset, Any]]
Matching pairs of (dataset, data element value)
This usually is a single pair, but a list is returned for future
ability to work across multiple files.
Note
----
This function is meant to be used in a call to an `argparse` library's
`add_argument` call for subparsers, with name="filespec" and
`type=filespec_parser`. When used that way, the resulting args.filespec
will contain the return values of this function
(e.g. use `ds, element_val = args.filespec` after parsing arguments)
See the `pydicom.cli.show` module for an example.
Raises
------
argparse.ArgumentTypeError
If the filename does not exist in local path or in pydicom test files,
or if the optional element is not a valid expression,
or if the optional element is a valid expression but does not exist
within the dataset
"""
prefix, filename, element = filespec_parts(filespec)
# Get the pydicom test filename even without prefix, in case user forgot it
try:
pydicom_filename = cast(str, get_testdata_file(filename))
except ValueError: # will get this if absolute path passed
pydicom_filename = ""
# Check if filename is in charset files
if not pydicom_filename:
try:
char_filenames = get_charset_files(filename)
if char_filenames:
pydicom_filename = char_filenames[0]
except NotImplementedError: # will get this if absolute path passed
pass
if prefix == "pydicom":
filename = pydicom_filename
# Check element syntax first to avoid unnecessary load of file
if element and not re_file_spec_object.match(element):
raise argparse.ArgumentTypeError(
f"Component '{element}' is not valid syntax for a "
"data element, sequence, or sequence item"
)
# Read DICOM file
try:
ds = dcmread(filename, force=True)
except FileNotFoundError:
extra = (
(f", \nbut 'pydicom::{filename}' test data file is available")
if pydicom_filename
else ""
)
raise argparse.ArgumentTypeError(f"File '{filename}' not found{extra}")
except Exception as e:
raise argparse.ArgumentTypeError(f"Error reading '{filename}': {e}")
if not element:
return [(ds, None)]
data_elem_val = eval_element(ds, element)
return [(ds, data_elem_val)]
def help_command(args: argparse.Namespace) -> None:
if subparsers is None:
print("No subcommands are available")
return
subcommands: list[str] = list(subparsers.choices.keys())
if args.subcommand and args.subcommand in subcommands:
subparsers.choices[args.subcommand].print_help()
else:
print("Use pydicom help [subcommand] to show help for a subcommand")
subcommands.remove("help")
print(f"Available subcommands: {', '.join(subcommands)}")
SubCommandType = dict[str, Callable[[argparse._SubParsersAction], None]]
def get_subcommand_entry_points() -> SubCommandType:
subcommands = {}
for entry_point in entry_points(group="pydicom_subcommands"):
subcommands[entry_point.name] = entry_point.load()
return subcommands
def main(args: list[str] | None = None) -> None:
"""Entry point for 'pydicom' command line interface
Parameters
----------
args : List[str], optional
Command-line arguments to parse. If ``None``, then :attr:`sys.argv`
is used.
"""
global subparsers
py_version = sys.version.split()[0]
parser = argparse.ArgumentParser(
prog="pydicom",
description=f"pydicom command line utilities (Python {py_version})",
)
subparsers = parser.add_subparsers(help="subcommand help")
help_parser = subparsers.add_parser("help", help="display help for subcommands")
help_parser.add_argument(
"subcommand", nargs="?", help="Subcommand to show help for"
)
help_parser.set_defaults(func=help_command)
# Get subcommands to register themselves as a subparser
subcommands = get_subcommand_entry_points()
for subcommand in subcommands.values():
subcommand(subparsers)
ns = parser.parse_args(args)
if not vars(ns):
parser.print_help()
else:
ns.func(ns)