diff -Nru pydicom-1.4.1/appveyor.yml pydicom-2.0.0/appveyor.yml --- pydicom-1.4.1/appveyor.yml 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/appveyor.yml 2020-05-29 01:44:31.000000000 +0000 @@ -5,10 +5,6 @@ # We run the tests on 2 different target platforms for testing purpose only. # We use miniconda versions of Python provided by appveyor windows images matrix: - - PYTHON: "C:\\Miniconda-x64" - PYTHON_VERSION: "2.7.x" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Miniconda35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" @@ -36,9 +32,7 @@ - "conda install -y -q setuptools" - "conda --version" - # the free channel is only needed for conda 4.7 and Python 2.7 # Miniconda 34 and 35 use an earlier version of conda (4.3) - - "IF '%PYTHON_VERSION%'=='2.7.x' conda config --set restore_free_channel true" - "conda install pip numpy nose wheel pytest matplotlib -y -q" - "pip install nose-timer nose-exclude" diff -Nru pydicom-1.4.1/build_tools/circle/build_doc.sh pydicom-2.0.0/build_tools/circle/build_doc.sh --- pydicom-1.4.1/build_tools/circle/build_doc.sh 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/build_tools/circle/build_doc.sh 2020-05-29 01:44:31.000000000 +0000 @@ -94,7 +94,7 @@ python3 -m venv venv . venv/bin/activate -pip install setuptools numpy matplotlib sphinx pillow sphinx_rtd_theme numpydoc sphinx-gallery +pip install setuptools numpy matplotlib sphinx pillow sphinx_rtd_theme numpydoc sphinx-gallery sphinx-issues~=1.0 # Build and install pydicom in dev mode pip install -e . diff -Nru pydicom-1.4.1/build_tools/sphinx/sphinx_issues.py pydicom-2.0.0/build_tools/sphinx/sphinx_issues.py --- pydicom-1.4.1/build_tools/sphinx/sphinx_issues.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/build_tools/sphinx/sphinx_issues.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -"""A Sphinx extension for linking to your project's issue tracker. - -Copyright 2014 Steven Loria - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Slightly adapted to add support for pull requests. -""" - -from docutils import nodes, utils -from sphinx.util.nodes import split_explicit_title - -__version__ = '0.2.0' -__author__ = 'Steven Loria' -__license__ = 'MIT' - - -def user_role(name, rawtext, text, lineno, - inliner, options=None, content=None): - """Sphinx role for linking to a user profile. Defaults to linking to - Github profiles, but the profile URIS can be configured via the - ``ref_user_uri`` config value. - - Example: :: - - :user:`sloria` - """ - options = options or {} - has_explicit_title, title, target = split_explicit_title(text) - - target = utils.unescape(target).strip() - title = utils.unescape(title).strip() - config = inliner.document.settings.env.app.config - if config.issues_user_uri: - ref = config.ref_user_uri.format(user=target) - else: - ref = 'https://github.com/{0}'.format(target) - if has_explicit_title: - text = title - else: - text = '@{0}'.format(target) - - link = nodes.reference(text=text, refuri=ref, **options) - return [link], [] - - -def _make_ref_node(ref_type, ref_no, config, uri=None, options=None): - options = options or {} - if ref_no not in ('-', '0'): - if uri: - ref = uri.format(ref_type=ref_type, ref_no=ref_no) - elif config.ref_github_path: - ref = 'https://github.com/{0}/{1}/{2}'.format( - config.ref_github_path, ref_type, ref_no - ) - ref_text = '#{0}'.format(ref_no) - link = nodes.reference(text=ref_text, refuri=ref, **options) - else: - link = None - return link - - -def _ref_role(ref_type, text, inliner, options=None): - """Sphinx role for linking to a pull request. Must have - `pr_uri` or `issues_github_path` configured in ``conf.py``. - """ - options = options or {} - ref_nos = [each.strip() for each in utils.unescape(text).split(',')] - config = inliner.document.settings.env.app.config - ret = [] - for i, ref_no in enumerate(ref_nos): - node = _make_ref_node(ref_type, ref_no, config, options=options) - ret.append(node) - if i != len(ref_nos) - 1: - sep = nodes.raw(text=', ', format='html') - ret.append(sep) - return ret, [] - - -def issue_role(name, rawtext, text, lineno, - inliner, options=None, content=None): - """Sphinx role for linking to an issue. Must have - `ref_uri` or `ref_github_path` configured in ``conf.py``. - - Examples: :: - - :issue:`123` - :issue:`42,45` - """ - return _ref_role('issues', text, inliner, options) - - -def pull_request_role(name, rawtext, text, lineno, - inliner, options=None, content=None): - """Sphinx role for linking to a pull request. Must have - `ref_uri` or `ref_github_path` configured in ``conf.py``. - - Examples: :: - - :pull_request:`123` - :pull_request:`42,45` - """ - return _ref_role('pull', text, inliner, options) - - -def setup(app): - # Format template for issues/pull request URI - # e.g. 'https://github.com/sloria/marshmallow/{ref_type}/{ref_no} - app.add_config_value('ref_uri', default=None, rebuild='html') - # Shortcut for Github, e.g. 'sloria/marshmallow' - app.add_config_value('ref_github_path', default=None, rebuild='html') - # Format template for user profile URI - # e.g. 'https://github.com/{user}' - app.add_config_value('ref_user_uri', default=None, rebuild='html') - app.add_role('issue', issue_role) - app.add_role('pull_request', pull_request_role) - app.add_role('user', user_role) diff -Nru pydicom-1.4.1/CONTRIBUTING.md pydicom-2.0.0/CONTRIBUTING.md --- pydicom-1.4.1/CONTRIBUTING.md 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/CONTRIBUTING.md 2020-05-29 01:44:31.000000000 +0000 @@ -2,10 +2,19 @@ Contributing to pydicom ======================= -This is the summary for contributing code, documentation, testing, and filing -issues. Please read it carefully to help making the code review process go as -smoothly as possible and maximize the likelihood of your contribution being -merged. +This is the guide for contributing code, documentation and tests, and for +filing issues. Please read it carefully to help make the code review +process go as smoothly as possible and maximize the likelihood of your +contribution being merged. + +_Note:_ +If you want to contribute new functionality, you may first consider if this +functionality belongs to the pydicom core, or is better suited for +[contrib-pydicom](https://github.com/pydicom/contrib-pydicom). contrib-pydicom +collects some convenient functionality that uses pydicom, but doesn't +belong to the pydicom core. If you're not sure where your contribution belongs, +create an issue where you can discuss this before creating a pull request. + How to contribute ----------------- @@ -41,13 +50,37 @@ $ git commit ``` - to record your changes in Git, then push the changes to your GitHub account with: +5. Add a meaningful commit message. Pull requests are "squash-merged", e.g. + squashed into one commit with all commit messages combined. The commit + messages can be edited during the merge, but it helps if they are clearly + and briefly showing what has been done in the commit. Check out the + [seven commonly accepted rules](https://www.theserverside.com/video/Follow-these-git-commit-message-guidelines) + for commit messages. Here are some examples, taken from actual commits: + + ``` + Add support for new VRs OV, SV, UV + + - closes #1016 + ``` + ``` + Add warning when saving compressed without encapsulation + ``` + ``` + Add optional handler argument to Dataset.decompress() + + - also add it to Dataset.convert_pixel_data() + - add separate error handling for given handle + - see #537 + ``` + +6. To record your changes in Git, push the changes to your GitHub + account with: ```bash $ git push -u origin my-feature ``` -5. Follow [these instructions](https://help.github.com/articles/creating-a-pull-request-from-a-fork) +7. Follow [these instructions](https://help.github.com/articles/creating-a-pull-request-from-a-fork) to create a pull request from your fork. This will send an email to the committers. (If any of the above seems like magic to you, please look up the @@ -59,31 +92,28 @@ We recommend that your contribution complies with the following rules before you submit a pull request: -- Follow the - [coding-guidelines](http://pydicom.org/dev/developers/contributing.html#coding-guidelines). - -- Use, when applicable, the validation tools and scripts in the - `pydicom.utils` submodule. A list of utility routines available - for developers can be found in the - [Utilities for Developers](http://pydicom.org/dev/developers/utilities.html#developers-utils) - page. - +- Follow the style used in the rest of the code. That mostly means to + follow [PEP-8 guidelines](https://www.python.org/dev/peps/pep-0008/) for + the code, and the [Google style](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings) + for documentation. + - If your pull request addresses an issue, please use the pull request title to describe the issue and mention the issue number in the pull request description. This will make sure a link back to the original issue is - created. Use "closes #PR-NUM" or "fixes #PR-NUM" to indicate github to - automatically close the related issue. Use any other keyword (i.e: works on, - related) to avoid github to close the referenced issue. + created. Use "closes #issue-number" or "fixes #issue-number" to let GitHub + automatically close the related issue on commit. Use any other keyword + (i.e. works on, related) to avoid GitHub to close the referenced issue. - All public methods should have informative docstrings with sample usage presented as doctests when appropriate. - Please prefix the title of your pull request with `[MRG]` (Ready for Merge), - if the contribution is complete and ready for a detailed review. Two core - developers will review your code and change the prefix of the pull request to - `[MRG + 1]` on approval, making it eligible for merging. An incomplete - contribution -- where you expect to do more work before receiving a full - review -- should be prefixed `[WIP]` (to indicate a work in progress) and + if the contribution is complete and ready for a detailed review. Some of the + core developers will review your code, make suggestions for changes, and + approve it as soon as it is ready for merge. Pull requests are usually merged + after two approvals by core developers, or other developers asked to review the code. + An incomplete contribution -- where you expect to do more work before receiving a full + review -- should be prefixed with `[WIP]` (to indicate a work in progress) and changed to `[MRG]` when it matures. WIPs may be useful to: indicate you are working on something to avoid duplicated work, request broad review of functionality or API, or seek collaborators. WIPs often benefit from the @@ -91,31 +121,24 @@ [task list](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) in the PR description. -- When adding additional functionality, provide at least one - example script in the ``examples/`` folder. Have a look at other +- When adding additional functionality, check if it makes sense to add one or + more example scripts in the ``examples/`` folder. Have a look at other examples for reference. Examples should demonstrate why the new functionality is useful in practice and, if possible, compare it to other methods available in pydicom. - Documentation and high-coverage tests are necessary for enhancements to be - accepted. Bug-fixes or new features should be provided with - [non-regression tests](https://en.wikipedia.org/wiki/Non-regression_testing). - These tests verify the correct behavior of the fix or feature. In this - manner, further modifications on the code base are granted to be consistent - with the desired behavior. - For the Bug-fixes case, at the time of the PR, this tests should fail for - the code base in master and pass for the PR code. - -- The documentation should also include expected time and space - complexity of the algorithm and scalability, e.g. "this algorithm - can scale to a large number of samples > 100000, but does not - scale in dimensionality: n_features is expected to be lower than - 100". + accepted. Bug-fixes shall be provided with + [regression tests](https://en.wikipedia.org/wiki/regression_testing) that + fail before the fix. For new features, the correct behavior shall be + verified by feature tests. A good practice to write sufficient tests is + [test-driven development](https://en.wikipedia.org/wiki/Test-driven_development). -You can also check for common programming errors with the following -tools: +You can also check for common programming errors and style issues with the +following tools: -- Code with good unittest **coverage** (at least 80%), check with: +- Code with good unittest **coverage** (current coverage or better), check + with: ```bash $ pip install pytest pytest-cov @@ -132,8 +155,8 @@ - No PEP8 warnings, check with: ```bash - $ pip install pep8 - $ pep8 path/to/module.py + $ pip install pycodestyle # formerly called pep8 + $ pycodestyle path/to/module.py ``` - AutoPEP8 can help you fix some of the easy redundant errors: @@ -143,13 +166,9 @@ $ autopep8 path/to/pep8.py ``` -Bonus points for contributions that include a performance analysis with -a benchmark script and profiling output (please report on the mailing -list or on the GitHub issue). - Filing bugs ----------- -We use Github issues to track all bugs and feature requests; feel free to +We use GitHub issues to track all bugs and feature requests; feel free to open an issue if you have found a bug or wish to see a feature implemented. It is recommended to check that your issue complies with the @@ -164,15 +183,23 @@ See [Creating and highlighting code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks). - Please include your operating system type and version number, as well - as your Python, pydicom and numpy versions. This information - can be found by running the following code snippet: + as your Python and pydicom versions. - ```python - import platform; print(platform.platform()) - import sys; print("Python", sys.version) - import numpy; print("numpy", numpy.__version__) - import pydicom; print("pydicom", pydicom.__version__) - ``` + If you're using **pydicom 2 or later**, please use the `pydicom_env_info` + module to gather this information : + + ```bash + $ python -m pydicom.env_info + ``` + + For **pydicom 1.x**, please run the following code snippet instead. + + ```python + import platform, sys, pydicom + print(platform.platform(), + "\nPython", sys.version, + "\npydicom", pydicom.__version__) + ``` - please include a [reproducible](http://stackoverflow.com/help/mcve) code snippet or link to a [gist](https://gist.github.com). If an exception is @@ -180,22 +207,11 @@ non beautified version of the trackeback) -New contributor tips --------------------- - -A great way to start contributing to pydicom is to pick an item -from the list of [Easy issues](https://github.com/pydicom/pydicom/issues?labels=Easy) -in the issue tracker. Resolving these issues allow you to start -contributing to the project without much prior knowledge. Your -assistance in this area will be greatly appreciated by the more -experienced developers as it helps free up their time to concentrate on -other issues. - Documentation ------------- We are glad to accept any sort of documentation: function docstrings, -reStructuredText documents (like this one), tutorials, etc. +reStructuredText documents, tutorials, etc. reStructuredText documents live in the source code repository under the ``doc/`` directory. @@ -207,13 +223,12 @@ ``README`` file in the ``doc/`` directory for more information. For building the documentation, you will need -[sphinx](http://sphinx.pocoo.org/), +[sphinx](https://www.sphinx-doc.org/), [numpy](http://numpy.org/), [matplotlib](http://matplotlib.org/), and [pillow](http://pillow.readthedocs.io/en/latest/). -When you are writing documentation, it is important to reference the related -part of the DICOM standard, and give give intuition to the reader on what the -method does. It is best to always start with a small paragraph with a -hand-waving explanation of what the method does to the data and a figure (coming -from an example) illustrating it. +When you are writing documentation that references DICOM, it is often +helpful to reference the related part of the +[DICOM standard](https://www.dicomstandard.org/current/). Try to make the +explanations intuitive and understandable also for users not fluent in DICOM. diff -Nru pydicom-1.4.1/debian/changelog pydicom-2.0.0/debian/changelog --- pydicom-1.4.1/debian/changelog 2020-01-22 12:57:10.000000000 +0000 +++ pydicom-2.0.0/debian/changelog 2020-07-17 13:43:36.000000000 +0000 @@ -1,3 +1,18 @@ +pydicom (2.0.0-1) unstable; urgency=medium + + * New upstream version + Closes: #964650 + * debhelper-compat 13 (routine-update) + * Secure URI in copyright format (routine-update) + * Remove trailing whitespace in debian/changelog (routine-update) + * Add salsa-ci file (routine-update) + * Rules-Requires-Root: no (routine-update) + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + * Add missing Build-Depends: python3-sphinx-issues + + -- Andreas Tille Fri, 17 Jul 2020 15:43:36 +0200 + pydicom (1.4.1-1) unstable; urgency=medium * Team upload. @@ -50,7 +65,7 @@ - dicom module was renamed to pydicom, hence binary package rename - provides dicom shim Python package to ease migrations * debian/control - - renamed packages to be -pydicom not -dicom. They now also + - renamed packages to be -pydicom not -dicom. They now also provide/replace and conflict with older python-dicom* - adjusted Homepage to point to https://pydicom.github.io (Closes: #902388) diff -Nru pydicom-1.4.1/debian/control pydicom-2.0.0/debian/control --- pydicom-1.4.1/debian/control 2020-01-22 12:57:10.000000000 +0000 +++ pydicom-2.0.0/debian/control 2020-07-17 13:43:36.000000000 +0000 @@ -5,7 +5,7 @@ Section: python Testsuite: autopkgtest-pkg-python Priority: optional -Build-Depends: debhelper-compat (= 10), +Build-Depends: debhelper-compat (= 13), dh-python, python3-all, python3-pytest, @@ -16,12 +16,14 @@ python3-sphinx, python3-sphinx-gallery, python3-sphinx-rtd-theme, + python3-sphinx-issues, python3-matplotlib, python3-numpydoc Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/med-team/pydicom Vcs-Git: https://salsa.debian.org/med-team/pydicom.git Homepage: https://pydicom.github.io +Rules-Requires-Root: no Package: python3-pydicom Architecture: all diff -Nru pydicom-1.4.1/debian/copyright pydicom-2.0.0/debian/copyright --- pydicom-1.4.1/debian/copyright 2020-01-22 12:57:10.000000000 +0000 +++ pydicom-2.0.0/debian/copyright 2020-07-17 13:43:36.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: pydicom Upstream-Contact: Darcy Mason Source: https://github.com/darcymason/pydicom diff -Nru pydicom-1.4.1/debian/patches/deb_no_unicode pydicom-2.0.0/debian/patches/deb_no_unicode --- pydicom-1.4.1/debian/patches/deb_no_unicode 2020-01-22 12:57:10.000000000 +0000 +++ pydicom-2.0.0/debian/patches/deb_no_unicode 2020-07-17 13:43:36.000000000 +0000 @@ -9,41 +9,27 @@ --- a/pydicom/tests/test_gdcm_pixel_data.py +++ b/pydicom/tests/test_gdcm_pixel_data.py -@@ -109,13 +109,13 @@ class TestGDCM_JPEG_LS_no_gdcm(object): +@@ -107,7 +107,7 @@ save_dir = os.getcwd() + class TestGDCM_JPEG_LS_no_gdcm: def setup(self): - if compat.in_py2: - self.utf8_filename = os.path.join( -- tempfile.gettempdir(), "ДИКОМ.dcm") -+ tempfile.gettempdir(), "DICOM.dcm") - self.unicode_filename = self.utf8_filename.decode("utf8") - shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"), - self.unicode_filename) - else: - self.unicode_filename = os.path.join( -- tempfile.gettempdir(), "ДИКОМ.dcm") -+ tempfile.gettempdir(), "DICOM.dcm") - shutil.copyfile(jpeg_ls_lossless_name, self.unicode_filename) + self.unicode_filename = os.path.join( +- tempfile.gettempdir(), "ДИКОМ.dcm") ++ tempfile.gettempdir(), "DICOM.dcm") + shutil.copyfile(jpeg_ls_lossless_name, self.unicode_filename) self.jpeg_ls_lossless = dcmread(self.unicode_filename) self.mr_small = dcmread(mr_name) -@@ -397,13 +397,13 @@ class TestsWithGDCM(object): +@@ -389,7 +389,7 @@ class TestsWithGDCM: @pytest.fixture(scope='class') def unicode_filename(self): - if compat.in_py2: -- utf8_filename = os.path.join(tempfile.gettempdir(), "ДИКОМ.dcm") -+ utf8_filename = os.path.join(tempfile.gettempdir(), "DICOM.dcm") - unicode_filename = utf8_filename.decode("utf8") - shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"), - unicode_filename) - else: - unicode_filename = os.path.join( -- tempfile.gettempdir(), "ДИКОМ.dcm") -+ tempfile.gettempdir(), "DICOM.dcm") - shutil.copyfile(jpeg_ls_lossless_name, unicode_filename) + unicode_filename = os.path.join( +- tempfile.gettempdir(), "ДИКОМ.dcm") ++ tempfile.gettempdir(), "DICOM.dcm") + shutil.copyfile(jpeg_ls_lossless_name, unicode_filename) yield unicode_filename os.remove(unicode_filename) --- a/pydicom/tests/test_filereader.py +++ b/pydicom/tests/test_filereader.py -@@ -114,7 +114,7 @@ class TestReader(object): +@@ -112,7 +112,7 @@ class TestReader: assert empty_number_tags_ds.VectorGridData is None def test_UTF8_filename(self): diff -Nru pydicom-1.4.1/debian/patches/fix_numpy_test.patch pydicom-2.0.0/debian/patches/fix_numpy_test.patch --- pydicom-1.4.1/debian/patches/fix_numpy_test.patch 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/debian/patches/fix_numpy_test.patch 2020-07-17 13:43:36.000000000 +0000 @@ -0,0 +1,18 @@ +Description: fix numpy test to use double quotes + Upstream seemingly reverted to using single quotes instead of double quotes, which throws an assertion during the test. +Author: Shayan Doust +Applied-Upstream: https://github.com/pydicom/pydicom/pull/1114/commits/2d6337bd770db09d627b77c9a1c354b1666ad39e +Last-Update: 2020-07-17 +--- + +--- pydicom.orig/pydicom/tests/test_handler_util.py ++++ pydicom/pydicom/tests/test_handler_util.py +@@ -945,7 +945,7 @@ + ds = dcmread(PAL_08_256_0_16_1F) + ds.RedPaletteColorLookupTableDescriptor[2] = 15 + msg = ( +- r'data type "uint15" not understood' ++ r"data type ['\"]uint15['\"] not understood" + ) + with pytest.raises(TypeError, match=msg): + apply_color_lut(ds.pixel_array, ds) diff -Nru pydicom-1.4.1/debian/patches/series pydicom-2.0.0/debian/patches/series --- pydicom-1.4.1/debian/patches/series 2020-01-22 12:57:10.000000000 +0000 +++ pydicom-2.0.0/debian/patches/series 2020-07-17 13:43:36.000000000 +0000 @@ -1 +1,2 @@ +fix_numpy_test.patch deb_no_unicode diff -Nru pydicom-1.4.1/debian/salsa-ci.yml pydicom-2.0.0/debian/salsa-ci.yml --- pydicom-1.4.1/debian/salsa-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/debian/salsa-ci.yml 2020-07-17 13:43:36.000000000 +0000 @@ -0,0 +1,4 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml diff -Nru pydicom-1.4.1/debian/upstream/metadata pydicom-2.0.0/debian/upstream/metadata --- pydicom-1.4.1/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/debian/upstream/metadata 2020-07-17 13:43:36.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/darcymason/pydicom/issues +Bug-Submit: https://github.com/darcymason/pydicom/issues/new +Repository: https://github.com/darcymason/pydicom.git +Repository-Browse: https://github.com/darcymason/pydicom diff -Nru pydicom-1.4.1/dicom.py pydicom-2.0.0/dicom.py --- pydicom-1.4.1/dicom.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/dicom.py 2020-05-29 01:44:31.000000000 +0000 @@ -5,7 +5,7 @@ Alternatively, most code can easily be converted to pydicom > 1.0 by changing import lines from 'import dicom' to 'import pydicom'. See the Transition Guide at -https://pydicom.github.io/pydicom/stable/transition_to_pydicom1.html. +https://pydicom.github.io/pydicom/stable/old/transition_to_pydicom1.html. """ raise ImportError(msg) diff -Nru pydicom-1.4.1/doc/conf.py pydicom-2.0.0/doc/conf.py --- pydicom-1.4.1/doc/conf.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/conf.py 2020-05-29 01:44:31.000000000 +0000 @@ -132,7 +132,7 @@ # General information about the project. project = u'pydicom' -copyright = u'2008-2019, Darcy Mason and pydicom contributors' +copyright = u'2008-2020, Darcy Mason and pydicom contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -303,10 +303,7 @@ # Config for sphinx_issues - -ref_uri = 'https://github.com/pydicom/pydicom/{ref_type}/{ref_no}' -ref_github_path = 'pydicom/pydicom' -ref_user_uri = 'https://github.com/{user}' +issues_github_path = 'pydicom/pydicom' def setup(app): diff -Nru pydicom-1.4.1/doc/guides/element_value_types.rst pydicom-2.0.0/doc/guides/element_value_types.rst --- pydicom-1.4.1/doc/guides/element_value_types.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/guides/element_value_types.rst 2020-05-29 01:44:31.000000000 +0000 @@ -34,9 +34,7 @@ * To ensure **AT** elements are encoded correctly, their values should be set using the 8-byte integer form of the tag - such as ``0x00100020`` for the tag (0010,0020) - and not as a 2-tuple or 2-list. -* **LO**, **LT**, **PN**, **SH**, **ST**, **UC** and **UT** elements may also - be set and stored using - `unicode `_ in Python 2. + +----+------------------+-----------------+-----------------------------------+ | VR | Name | Set using | Stored as | @@ -86,10 +84,7 @@ +----+------------------+-----------------+-----------------------------------+ | OW | Other Word | :class:`bytes` | :class:`bytes` | +----+------------------+-----------------+-----------------------------------+ -| PN | Person Name | :class:`str` | :class:`str`\ :sup:`3` | -| | | | (Python 2) or | -| | | | :class:`~valuerep.PersonName3` | -| | | | (Python 3) | +| PN | Person Name | :class:`str` | :class:`~valuerep.PersonName` | +----+------------------+-----------------+-----------------------------------+ | SH | Short String | :class:`str` | :class:`str` | +----+------------------+-----------------+-----------------------------------+ @@ -132,5 +127,3 @@ = ``True`` (default ``False``) | :sup:`2` If :attr:`config.use_DS_decimal` = ``True`` (default ``False``) -| :sup:`3` **PN** element values read from file will be stored as - :class:`~valuerep.PersonNameUnicode` in Python 2 diff -Nru pydicom-1.4.1/doc/old/base_element.rst pydicom-2.0.0/doc/old/base_element.rst --- pydicom-1.4.1/doc/old/base_element.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/old/base_element.rst 2020-05-29 01:44:31.000000000 +0000 @@ -27,15 +27,23 @@ usually get one by reading an existing DICOM file:: >>> import pydicom - >>> from pydicom.data import get_testdata_files + >>> from pydicom.data import get_testdata_file >>> # get some test data - >>> filename = get_testdata_files("rtplan.dcm")[0] + >>> filename = get_testdata_file("rtplan.dcm") >>> ds = pydicom.dcmread(filename) You can display the entire dataset by simply printing its string (:class:`str()` or :func:`repr`) value:: >>> ds # doctest: +ELLIPSIS + Dataset.file_meta ------------------------------- + (0002, 0000) File Meta Information Group Length UL: 156 + (0002, 0001) File Meta Information Version OB: b'\x00\x01' + (0002, 0002) Media Storage SOP Class UID UI: RT Plan Storage + (0002, 0003) Media Storage SOP Instance UID UI: 1.2.999.999.99.9.9999.9999.20030903150023 + (0002, 0010) Transfer Syntax UID UI: Implicit VR Little Endian + (0002, 0012) Implementation Class UID UI: 1.2.888.888.88.8.8.8 + ------------------------------------------------- (0008, 0012) Instance Creation Date DA: '20030903' (0008, 0013) Instance Creation Time TM: '150031' (0008, 0016) SOP Class UID UI: RT Plan Storage @@ -185,7 +193,7 @@ through the `PixelData` keyword:: >>> # read data with actual pixel data - >>> filename = get_testdata_files("CT_small.dcm")[0] + >>> filename = get_testdata_file("CT_small.dcm") >>> ds = pydicom.dcmread(filename) >>> pixel_bytes = ds.PixelData @@ -232,8 +240,7 @@ :func:`BaseTags` are automatically created when you assign or read elements using their keywords as illustrated in sections above. -The :class:`~tag.BaseTag` class is derived from :class:`int` in Python 3 and -`long `_ in Python 2, +The :class:`~tag.BaseTag` class is derived from :class:`int`, so in effect, it is just a number with some extra behaviour: * :func:`~tag.Tag` is used to create instances of :class:`~tag.BaseTag` and diff -Nru pydicom-1.4.1/doc/old/image_data_handlers.rst pydicom-2.0.0/doc/old/image_data_handlers.rst --- pydicom-1.4.1/doc/old/image_data_handlers.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/old/image_data_handlers.rst 2020-05-29 01:44:31.000000000 +0000 @@ -41,6 +41,8 @@ >>> ds = dcmread('path/to/dicom/file') >>> ds.file_meta.TransferSyntaxUID '1.2.840.10008.1.2.1' + >>> ds.BitsAllocated + 16 As far as we have been able to verify, the following transfer syntaxes are handled by the given packages: @@ -62,11 +64,12 @@ +------------------------------------+------------------------+-------+---------+---------+-----------------+ | JPEG Baseline (Process 1) | 1.2.840.10008.1.2.4.50 | | | |chk| | |chk|\ :sup:`1` | +------------------------------------+------------------------+-------+---------+---------+-----------------+ -| JPEG Extended (Process 2 and 4) | 1.2.840.10008.1.2.4.51 | | | |chk| | |chk|\ :sup:`1` | +| JPEG Extended (Process 2 and 4) | 1.2.840.10008.1.2.4.51 | | | |chk| | |chk|\ | +| | | | | | :sup:`1,3` | +------------------------------------+------------------------+-------+---------+---------+-----------------+ | JPEG Lossless (Process 14) | 1.2.840.10008.1.2.4.57 | | | |chk| | | +------------------------------------+------------------------+-------+---------+---------+-----------------+ -| JPEG Lossless (Process 14, SV1) | 1.2.840.10008.1.2.4.70 | | | |chk| | |chk|\ :sup:`3` | +| JPEG Lossless (Process 14, SV1) | 1.2.840.10008.1.2.4.70 | | | |chk| | | +------------------------------------+------------------------+-------+---------+---------+-----------------+ | JPEG LS Lossless | 1.2.840.10008.1.2.4.80 | | |chk| | |chk| | | +------------------------------------+------------------------+-------+---------+---------+-----------------+ @@ -74,7 +77,7 @@ +------------------------------------+------------------------+-------+---------+---------+-----------------+ | JPEG2000 Lossless | 1.2.840.10008.1.2.4.90 | | | |chk| | |chk|\ :sup:`2` | +------------------------------------+------------------------+-------+---------+---------+-----------------+ -| JPEG2000 | 1.2.840.10008.1.2.4.91 | | | |chk| | |chk|\ :sup:`2` | +| JPEG2000 | 1.2.840.10008.1.2.4.91 | | | |chk| | |chk|\ :sup:`2` | +------------------------------------+------------------------+-------+---------+---------+-----------------+ | JPEG2000 Multi-component Lossless | 1.2.840.10008.1.2.4.92 | | | | | +------------------------------------+------------------------+-------+---------+---------+-----------------+ @@ -83,7 +86,7 @@ | :sup:`1` *only with JpegImagePlugin* | :sup:`2` *only with Jpeg2KImagePlugin* -| :sup:`3` *not supported for > 8 bit* +| :sup:`3` *only if (0028,0100) Bits Allocated = 8* Usage ..... diff -Nru pydicom-1.4.1/doc/old/private_data_elements.rst pydicom-2.0.0/doc/old/private_data_elements.rst --- pydicom-1.4.1/doc/old/private_data_elements.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/old/private_data_elements.rst 2020-05-29 01:44:31.000000000 +0000 @@ -33,10 +33,20 @@ 'CT_small.dcm':: >>> from pydicom import dcmread - >>> from pydicom.data import get_testdata_files - >>> ct_filename = get_testdata_files("CT_small")[0] + >>> from pydicom.data import get_testdata_file + >>> ct_filename = get_testdata_file("CT_small.dcm") >>> ds = dcmread(ct_filename) >>> ds + Dataset.file_meta ------------------------------- + (0002, 0000) File Meta Information Group Length UL: 192 + (0002, 0001) File Meta Information Version OB: b'\x00\x01' + (0002, 0002) Media Storage SOP Class UID UI: CT Image Storage + (0002, 0003) Media Storage SOP Instance UID UI: 1.3.6.1.4.1.5962.1.1.1.1.1.20040119072730.12322 + (0002, 0010) Transfer Syntax UID UI: Explicit VR Little Endian + (0002, 0012) Implementation Class UID UI: 1.3.6.1.4.1.5962.2 + (0002, 0013) Implementation Version Name SH: 'DCTOOL100' + (0002, 0016) Source Application Entity Title AE: 'CLUNIE1' + ------------------------------------------------- (0008, 0005) Specific Character Set CS: 'ISO_IR 100' (0008, 0008) Image Type CS: ['ORIGINAL', 'PRIMARY', 'AX IAL'] diff -Nru pydicom-1.4.1/doc/old/python2_support.rst pydicom-2.0.0/doc/old/python2_support.rst --- pydicom-1.4.1/doc/old/python2_support.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/old/python2_support.rst 2020-05-29 01:44:31.000000000 +0000 @@ -1,36 +1,16 @@ .. _Python2_support: -Python 2 Support Plan for Pydicom -================================= +Python 2 Support +================ -.. rubric:: Timeline of support for Python 2 +.. rubric:: Python 2 and *pydicom* -The Python developers have stated that Python 2 will no longer be supported -starting Jan 1, 2020. Numpy is also dropping support for Python 2 at that time. -Pytest, which pydicom uses for testing, will support only Python 3.5+ starting -with the pytest 5.0 release, but are supporting the 4.6 version until mid 2020. +*pydicom* dropped support for Python 2 following the 1.4.X versions. -It is clear -- Python 2 will become a thing of the past starting Jan 2020. -All packages, including pydicom, need to think of transitioning away from -Python 2 and supporting only Python 3. - -Pydicom code was written with common code for both Python 2.7 and Python 3 as -much as possible. Where necessary, checks for Python 2 have been used to -create small blocks of distinct code. When the time comes, it should be -relatively easy to remove the Python 2 blocks and leave a Python-3-only -version of pydicom. - -After some discussion on github, the proposed plan for pydicom and Python 2 -support is as follows - -* pydicom v1.3 (July 2019) - no changes to Python versions supported. Adds a - deprecation warning (to be deprecated after next major release) when run under Python 2 -* pydicom v1.4 (planned for release ~December 2019) will support Python 2.7 and Python 3.5+, - with Python 2 deprecation warning for following major release. * pydicom v2.0 (planned for ~April 2020) will be Python 3.5+ only * pydicom v2.1 (no date set) will be Python 3.6+ -We may consider the possibility of backporting some fixes to pydicom v1.4 for +We may consider the possibility of backporting some fixes to *pydicom* v1.4 for very serious issues, if users make the case for it. Generally speaking, -however, we encourage all pydicom users to make the transition to Python 3 by +however, we encourage all *pydicom* users to make the transition to Python 3 by early 2020. diff -Nru pydicom-1.4.1/doc/reference/config.rst pydicom-2.0.0/doc/reference/config.rst --- pydicom-1.4.1/doc/reference/config.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/reference/config.rst 2020-05-29 01:44:31.000000000 +0000 @@ -19,4 +19,10 @@ overlay_data_handlers pixel_data_handlers reset_data_element_callback + show_file_meta + DS_decimal + DS_numpy use_DS_decimal + use_IS_numpy + use_DS_numpy + diff -Nru pydicom-1.4.1/doc/reference/dataset.rst pydicom-2.0.0/doc/reference/dataset.rst --- pydicom-1.4.1/doc/reference/dataset.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/reference/dataset.rst 2020-05-29 01:44:31.000000000 +0000 @@ -12,5 +12,6 @@ Dataset FileDataset + FileMetaDataset PrivateBlock validate_file_meta diff -Nru pydicom-1.4.1/doc/reference/elem.dataelem.rst pydicom-2.0.0/doc/reference/elem.dataelem.rst --- pydicom-1.4.1/doc/reference/elem.dataelem.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/reference/elem.dataelem.rst 2020-05-29 01:44:31.000000000 +0000 @@ -12,5 +12,4 @@ DataElement DataElement_from_raw - isMultiValue RawDataElement diff -Nru pydicom-1.4.1/doc/reference/elem.valuerep.rst pydicom-2.0.0/doc/reference/elem.valuerep.rst --- pydicom-1.4.1/doc/reference/elem.valuerep.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/reference/elem.valuerep.rst 2020-05-29 01:44:31.000000000 +0000 @@ -19,7 +19,5 @@ IS MultiString PersonName - PersonName3 - PersonNameBase PersonNameUnicode TM diff -Nru pydicom-1.4.1/doc/release_notes/index.rst pydicom-2.0.0/doc/release_notes/index.rst --- pydicom-1.4.1/doc/release_notes/index.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/release_notes/index.rst 2020-05-29 01:44:31.000000000 +0000 @@ -2,6 +2,7 @@ Release notes ============= +.. include:: v2.0.0.rst .. include:: v1.4.1.rst .. include:: v1.4.0.rst .. include:: v1.3.0.rst diff -Nru pydicom-1.4.1/doc/release_notes/v1.2.0.rst pydicom-2.0.0/doc/release_notes/v1.2.0.rst --- pydicom-1.4.1/doc/release_notes/v1.2.0.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/release_notes/v1.2.0.rst 2020-05-29 01:44:31.000000000 +0000 @@ -34,15 +34,15 @@ * Updated DICOM dictionary for 2018c edition (:issue:`677`) * Added possibility to set byte strings as value for VRs that use only the default character set (:issue:`624`) -* Functions for encapsulating frames added to ``encaps`` module (:pull_request:`696`) +* Functions for encapsulating frames added to ``encaps`` module (:pr:`696`) * Added ``Dataset.fix_meta_info()`` (:issue:`584`) * Added new function for bit packing ``pack_bits`` for use with BitsAllocated - = 1 (:pull_request:`715`) + = 1 (:pr:`715`) * Added/corrected encoding and decoding of text and person name VRs using character sets with code extensions, added handling of encoding/decoding errors (:issue:`716`) * Handle common spelling errors in Specific Character Set values - (:pull_request:`695,737`) + (:pr:`695,737`) * Added ``uid.JPEGLosslessP14`` for UID 1.2.840.10008.1.2.4.57 * Added ``uid.JPEG2000MultiComponentLossless`` for UID 1.2.840.10008.1.2.4.92 * Added ``uid.JPEG2000MultiComponent`` for UID 1.2.840.10008.1.2.4.93 @@ -50,25 +50,25 @@ * Added support for single frame pixel data where BitsAllocated > 8 and SamplesPerPixel > 1 (:issue:`713`) * Small improvement in RLE decoding speed (~10%) -* Added support for non-conformant RLE segment ordering (:pull_request:`729`) +* Added support for non-conformant RLE segment ordering (:pr:`729`) Fixes ..... * Removed unused ``original_string`` attribute from the ``DataElement`` class - (:pull_request:`660`) + (:pr:`660`) * Improve performance for Python 3 when dealing with compressed multi-frame Pixel Data with pillow and jpeg-ls (:issue:`682`) * Fixed handling of private tags in repeater range (:issue:`689`) * Fixed Pillow pixel data handler for non-JPEG2k transfer syntax (:issue:`663`) -* Fixed handling of elements with ambiguous VR (:pull_request:`700, 728`) +* Fixed handling of elements with ambiguous VR (:pr:`700, 728`) * Adapted pixel handlers where endianess is explicitly adapted (:issue:`704`) -* Improve performance of bit unpacking (:pull_request:`715`) +* Improve performance of bit unpacking (:pr:`715`) * First character set no longer removed (:issue:`707`) -* Fixed RLE decoded data having the wrong byte order (:pull_request:`729`) +* Fixed RLE decoded data having the wrong byte order (:pr:`729`) * Fixed RLE decoded data having the wrong planar configuration - (:pull_request:`729`) + (:pr:`729`) * Fixed numpy arrays returned by the pixel data handlers sometimes being read-only. Read-only arrays are still available for uncompressed transfer syntaxes via a keyword argument for the numpy pixel data handler and should diff -Nru pydicom-1.4.1/doc/release_notes/v1.3.0.rst pydicom-2.0.0/doc/release_notes/v1.3.0.rst --- pydicom-1.4.1/doc/release_notes/v1.3.0.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/release_notes/v1.3.0.rst 2020-05-29 01:44:31.000000000 +0000 @@ -9,11 +9,11 @@ * New User Guide page for Python 2 support timeline * New User Guide page for working with private data elements * example loading set of CT slices and plotting axial, sagittal - and coronal (:pull_request:`789`) + and coronal (:pr:`789`) Changes ....... -* Removed deprecated uid variables, config.image_handlers and DeferredDataElement (:pull_request:`760`) +* Removed deprecated uid variables, config.image_handlers and DeferredDataElement (:pr:`760`) * ``dataelem.isMultiValue`` is deprecated and will be removed in v1.4. Use ``dataelem.DataElement.VM`` instead. * ``dataelem.isStringOrStringList`` and ``dataelem.isString`` functions are @@ -30,7 +30,7 @@ ``datadict.add_private_dict_entries`` to add custom private tags (:issue:`799`) * Added possibility to write into zip file using gzip, by avoiding seek (:issue:`753`) -* Added RLE encoding (:pull_request:`730`) +* Added RLE encoding (:pr:`730`) * Added handling of incorrect transfer syntax (explicit vs implicit) (:issue:`820`) * Added creation of Tag instances by DICOM keyword, e.g Tag("PatientName") diff -Nru pydicom-1.4.1/doc/release_notes/v2.0.0.rst pydicom-2.0.0/doc/release_notes/v2.0.0.rst --- pydicom-1.4.1/doc/release_notes/v2.0.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/doc/release_notes/v2.0.0.rst 2020-05-29 01:44:31.000000000 +0000 @@ -0,0 +1,61 @@ +Version 2.0.0 +================================= + +Changelog +--------- +* Dropped support for Python 2 (only Python 3.5+ supported) + +* Changes to `Dataset.file_meta` + + * file_meta now shown by default in dataset `str` or `repr` output; + :data:`pydicom.config.show_file_meta` can be set ``False`` to restore + previous behavior + + * new :class:`~pydicom.dataset.FileMetaDataset` class that accepts + only group 2 data elements + + * Deprecation warning given unless `Dataset.file_meta` set with + a :class:`~pydicom.dataset.FileMetaDataset` object (in *pydicom* 3, + it will be required) + +* Old `PersonName` class removed; `PersonName3` renamed to `PersonName`. + Classes `PersonNameUnicode` and `PersonName3` are aliased to `PersonName` but + are deprecated and will be removed in version 2.1 +* ``dataelem.isMultiValue`` (previously deprecated) has been removed. + Use ``dataelem.DataElement.VM`` instead. + +Enhancements +............ +* Allow PathLike objects for filename argument in `dcmread`, `dcmwrite` and + `Dataset.save_as` (:issue:`1047`) +* Deflate post-file meta information data when writing a dataset with the + Deflated Explicit VR Little Endian transfer syntax UID (:issue:`1086`) +* Added `config.replace_un_with_known_vr` to be able to switch off automatic + VR conversion for known tags with VR "UN" (see :issue:`1067`) +* Added `config.use_DS_numpy` and `config.use_IS_numpy` to have multi-valued + data elements with VR of **DS** or **IS** return a numpy array (:issue:`623`) + (much faster for bigger arrays). Both default to False to preserve previous + behavior + +Fixes +..... +* Fixed reading of datasets with an empty `Specific Character Set` tag + (regression, :issue:`1038`) +* Fixed failure to parse dataset with an empty *LUT Descriptor* or + *Red/Green/Blue Palette Color LUT Descriptor* element. (:issue:`1049`) +* Made `Dataset.save_as` a wrapper for `dcmwrite` (:issue:`1042`) rather than + having different checks in each +* Removed ``1.2.840.10008.1.2.4.70`` - JPEG Lossless (Process 14, SV1) from + the Pillow pixel data handler as Pillow doesn't support JPEG Lossless. + (:issue:`1053`) +* Fixed error when writing elements with a VR of **OF** (:issue:`1075`) +* Fixed improper conversion when reading elements with a VR of **OF** + (:issue:`1075`) +* Fixed :func:`~pydicom.pixel_data_handlers.util.apply_voi_lut` and + :func:`~pydicom.pixel_data_handlers.util.apply_modality_lut` not handling + (0028,3006) *LUT Data* with a VR of **OW** (:issue:`1073`) +* Fixed access to private creator tag in raw datasets (:issue:`1078`) +* Fixed description of newly added known private tag (:issue:`1082`) +* Fixed update of private blocks after deleting private creator (:issue:`1097`) +* Fixed bug in updating `pydicom.config.use_DS_Decimal` flag + in :func:`~pydicom.config.DS_decimal` diff -Nru pydicom-1.4.1/doc/tutorials/dataset_basics.rst pydicom-2.0.0/doc/tutorials/dataset_basics.rst --- pydicom-1.4.1/doc/tutorials/dataset_basics.rst 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/doc/tutorials/dataset_basics.rst 2020-05-29 01:44:31.000000000 +0000 @@ -136,6 +136,16 @@ You can view the contents of the entire dataset by using :func:`print`:: >>> print(ds) + Dataset.file_meta ------------------------------- + (0002, 0000) File Meta Information Group Length UL: 192 + (0002, 0001) File Meta Information Version OB: b'\x00\x01' + (0002, 0002) Media Storage SOP Class UID UI: CT Image Storage + (0002, 0003) Media Storage SOP Instance UID UI: 1.3.6.1.4.1.5962.1.1.1.1.1.20040119072730.12322 + (0002, 0010) Transfer Syntax UID UI: Explicit VR Little Endian + (0002, 0012) Implementation Class UID UI: 1.3.6.1.4.1.5962.2 + (0002, 0013) Implementation Version Name SH: 'DCTOOL100' + (0002, 0016) Source Application Entity Title AE: 'CLUNIE1' + ------------------------------------------------- (0008, 0005) Specific Character Set CS: 'ISO_IR 100' (0008, 0008) Image Type CS: ['ORIGINAL', 'PRIMARY', 'AXIAL'] (0008, 0012) Instance Creation Date DA: '20040119' @@ -325,7 +335,7 @@ * Followed by a 4 byte ``DICM`` prefix * Followed by the required DICOM :dcm:`File Meta Information ` elements, which in *pydicom* are - stored in a :class:`~pydicom.dataset.Dataset` instance in the + stored in a :class:`~pydicom.dataset.FileMetaDataset` instance in the :attr:`~pydicom.dataset.FileDataset.file_meta` attribute:: >>> ds.file_meta @@ -584,7 +594,7 @@ Because we deleted the :attr:`~pydicom.dataset.FileDataset.file_meta` dataset we need to add it back:: - >>> ds.file_meta = Dataset() + >>> ds.file_meta = FileMetaDataset() And now we can add our *Transfer Syntax UID* element and save to file:: diff -Nru pydicom-1.4.1/examples/dicomtree.py pydicom-2.0.0/examples/dicomtree.py --- pydicom-1.4.1/examples/dicomtree.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/dicomtree.py 2020-05-29 01:44:31.000000000 +0000 @@ -12,14 +12,8 @@ python3 dicomtree.py """ -from __future__ import print_function -from pydicom import compat - -if compat.in_py2: - import Tix as tkinter_tix -else: - import tkinter.tix as tkinter_tix +import tkinter.tix as tkinter_tix print(__doc__) @@ -64,8 +58,8 @@ # order the dicom tags for data_element in dataset: node_id = parent + "." + hex(id(data_element)) - if isinstance(data_element.value, compat.text_type): - tree.hlist.add(node_id, text=compat.text_type(data_element)) + if isinstance(data_element.value, str): + tree.hlist.add(node_id, text=str(data_element)) else: tree.hlist.add(node_id, text=str(data_element)) if hide: diff -Nru pydicom-1.4.1/examples/input_output/plot_printing_dataset.py pydicom-2.0.0/examples/input_output/plot_printing_dataset.py --- pydicom-1.4.1/examples/input_output/plot_printing_dataset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/input_output/plot_printing_dataset.py 2020-05-29 01:44:31.000000000 +0000 @@ -10,8 +10,6 @@ # authors : Guillaume Lemaitre # license : MIT -from __future__ import print_function - import pydicom from pydicom.data import get_testdata_files diff -Nru pydicom-1.4.1/examples/input_output/plot_read_rtplan.py pydicom-2.0.0/examples/input_output/plot_read_rtplan.py --- pydicom-1.4.1/examples/input_output/plot_read_rtplan.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/input_output/plot_read_rtplan.py 2020-05-29 01:44:31.000000000 +0000 @@ -10,8 +10,6 @@ # authors : Guillaume Lemaitre # license : MIT -from __future__ import print_function - import pydicom from pydicom.data import get_testdata_files diff -Nru pydicom-1.4.1/examples/input_output/plot_write_dicom.py pydicom-2.0.0/examples/input_output/plot_write_dicom.py --- pydicom-1.4.1/examples/input_output/plot_write_dicom.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/input_output/plot_write_dicom.py 2020-05-29 01:44:31.000000000 +0000 @@ -17,7 +17,7 @@ import datetime import pydicom -from pydicom.dataset import Dataset, FileDataset +from pydicom.dataset import Dataset, FileDataset, FileMetaDataset # Create some temporary filenames suffix = '.dcm' @@ -26,7 +26,7 @@ print("Setting file meta information...") # Populate required values for file meta information -file_meta = Dataset() +file_meta = FileMetaDataset() file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' file_meta.MediaStorageSOPInstanceUID = "1.2.3" file_meta.ImplementationClassUID = "1.2.3.4" diff -Nru pydicom-1.4.1/examples/memory_dataset.py pydicom-2.0.0/examples/memory_dataset.py --- pydicom-1.4.1/examples/memory_dataset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/memory_dataset.py 2020-05-29 01:44:31.000000000 +0000 @@ -10,7 +10,6 @@ """ -from __future__ import print_function from io import BytesIO @@ -45,7 +44,7 @@ return dataset -class DummyDataBase(object): +class DummyDataBase: def __init__(self): self._blobs = {} diff -Nru pydicom-1.4.1/examples/metadata_processing/plot_add_dict_entries.py pydicom-2.0.0/examples/metadata_processing/plot_add_dict_entries.py --- pydicom-1.4.1/examples/metadata_processing/plot_add_dict_entries.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/metadata_processing/plot_add_dict_entries.py 2020-05-29 01:44:31.000000000 +0000 @@ -18,7 +18,6 @@ # Guillaume Lemaitre # license : MIT -from __future__ import print_function from pydicom.datadict import DicomDictionary, keyword_dict from pydicom.dataset import Dataset diff -Nru pydicom-1.4.1/examples/metadata_processing/plot_anonymize.py pydicom-2.0.0/examples/metadata_processing/plot_anonymize.py --- pydicom-1.4.1/examples/metadata_processing/plot_anonymize.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/metadata_processing/plot_anonymize.py 2020-05-29 01:44:31.000000000 +0000 @@ -13,7 +13,6 @@ # authors : Guillaume Lemaitre # license : MIT -from __future__ import print_function import tempfile diff -Nru pydicom-1.4.1/examples/show_charset_name.py pydicom-2.0.0/examples/show_charset_name.py --- pydicom-1.4.1/examples/show_charset_name.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/examples/show_charset_name.py 2020-05-29 01:44:31.000000000 +0000 @@ -10,13 +10,9 @@ # authors : Guillaume Lemaitre # license : MIT -from pydicom import compat from pydicom.valuerep import PersonNameUnicode -if compat.in_py2: - import Tkinter as tkinter -else: - import tkinter +import tkinter print(__doc__) diff -Nru pydicom-1.4.1/.github/FUNDING.yml pydicom-2.0.0/.github/FUNDING.yml --- pydicom-1.4.1/.github/FUNDING.yml 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/.github/FUNDING.yml 2020-05-29 01:44:31.000000000 +0000 @@ -0,0 +1 @@ +github: darcymason \ No newline at end of file diff -Nru pydicom-1.4.1/.github/ISSUE_TEMPLATE/bug_report.md pydicom-2.0.0/.github/ISSUE_TEMPLATE/bug_report.md --- pydicom-1.4.1/.github/ISSUE_TEMPLATE/bug_report.md 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/.github/ISSUE_TEMPLATE/bug_report.md 2020-05-29 01:44:31.000000000 +0000 @@ -19,9 +19,19 @@ traceback (if any) and the anonymized DICOM dataset (if relevant). **Your environment** -Please run the following and paste the output. +If you're using **pydicom 2 or later**, please use the `pydicom.env_info` +module to gather information about your environment and paste it in the issue: + ```bash -$ python -c "import platform; print(platform.platform())" -$ python -c "import sys; print('Python ', sys.version)" -$ python -c "import pydicom; print('pydicom ', pydicom.__version__)" +$ python -m pydicom.env_info ``` + +For **pydicom 1.x**, please run the following code snippet and paste the +output. + +```python +import platform, sys, pydicom +print(platform.platform(), + "\nPython", sys.version, + "\npydicom", pydicom.__version__) +``` \ No newline at end of file diff -Nru pydicom-1.4.1/.github/ISSUE_TEMPLATE/pixel_issue.md pydicom-2.0.0/.github/ISSUE_TEMPLATE/pixel_issue.md --- pydicom-1.4.1/.github/ISSUE_TEMPLATE/pixel_issue.md 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/.github/ISSUE_TEMPLATE/pixel_issue.md 2020-05-29 01:44:31.000000000 +0000 @@ -26,9 +26,19 @@ 4. The anonymized DICOM dataset (if possible). **Your environment** -Please run the following and paste the output. +If you're using **pydicom 2 or later**, please use the `pydicom.env_info` +module to gather information about your environment and paste it in the issue: + ```bash -$ python -c "import platform; print(platform.platform())" -$ python -c "import sys; print('Python ', sys.version)" -$ python -c "import pydicom; print('pydicom ', pydicom.__version__)" +$ python -m pydicom.env_info +``` + +For **pydicom 1.x**, please run the following code snippet and paste the +output. + +```python +import platform, sys, pydicom +print(platform.platform(), + "\nPython", sys.version, + "\npydicom", pydicom.__version__) ``` diff -Nru pydicom-1.4.1/LICENSE pydicom-2.0.0/LICENSE --- pydicom-1.4.1/LICENSE 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/LICENSE 2020-05-29 01:44:31.000000000 +0000 @@ -1,6 +1,6 @@ License file for pydicom, a pure-python DICOM library -Copyright (c) 2008-2018 Darcy Mason and pydicom contributors +Copyright (c) 2008-2020 Darcy Mason and pydicom contributors Except for portions outlined below, pydicom is released under an MIT license: diff -Nru pydicom-1.4.1/pydicom/benchmarks/bench_encaps.py pydicom-2.0.0/pydicom/benchmarks/bench_encaps.py --- pydicom-1.4.1/pydicom/benchmarks/bench_encaps.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/benchmarks/bench_encaps.py 2020-05-29 01:44:31.000000000 +0000 @@ -14,7 +14,7 @@ JP2K_10FRAME = get_testdata_files('emri_small_jpeg_2k_lossless.dcm')[0] -class TimeFragmentFrame(object): +class TimeFragmentFrame: """Time tests for the encaps.fragment_frame function.""" def setup(self): """Setup the test""" @@ -36,7 +36,7 @@ pass -class TimeItemiseFrame(object): +class TimeItemiseFrame: """Time tests for the encaps.itemise_frame function.""" def setup(self): """Setup the test""" @@ -58,7 +58,7 @@ pass -class TimeEncapsulate(object): +class TimeEncapsulate: """Time tests for the encaps.encapsulate function.""" def setup(self): """Setup the test""" diff -Nru pydicom-1.4.1/pydicom/benchmarks/bench_handler_numpy.py pydicom-2.0.0/pydicom/benchmarks/bench_handler_numpy.py --- pydicom-1.4.1/pydicom/benchmarks/bench_handler_numpy.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/benchmarks/bench_handler_numpy.py 2020-05-29 01:44:31.000000000 +0000 @@ -10,7 +10,7 @@ import numpy as np from pydicom import dcmread -from pydicom.dataset import Dataset +from pydicom.dataset import Dataset, FileMetaDataset from pydicom.data import get_testdata_files from pydicom.encaps import decode_data_sequence from pydicom.pixel_data_handlers.numpy_handler import get_pixeldata @@ -67,7 +67,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian ds.SOPClassUID = '1.2.3.4' ds.SOPInstanceUID = generate_uid() @@ -96,7 +96,7 @@ return tfile -class TimeGetPixelData_LargeDataset(object): +class TimeGetPixelData_LargeDataset: """Time tests for numpy_handler.get_pixeldata with large datasets.""" def setup(self): """Setup the tests.""" @@ -110,7 +110,7 @@ get_pixeldata(self.ds_16_3_100) -class TimeGetPixelData(object): +class TimeGetPixelData: """Time tests for numpy_handler.get_pixeldata.""" def setup(self): """Setup the tests.""" diff -Nru pydicom-1.4.1/pydicom/benchmarks/bench_handler_rle_decode.py pydicom-2.0.0/pydicom/benchmarks/bench_handler_rle_decode.py --- pydicom-1.4.1/pydicom/benchmarks/bench_handler_rle_decode.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/benchmarks/bench_handler_rle_decode.py 2020-05-29 01:44:31.000000000 +0000 @@ -36,7 +36,7 @@ RTDOSE_RLE_15F = get_testdata_files("rtdose_rle.dcm")[0] -class TimeRLEDecodeFrame(object): +class TimeRLEDecodeFrame: """Time tests for rle_handler._rle_decode_frame.""" def setup(self): # MONOCHROME2, 64x64, 1 sample/pixel, 16 bits allocated, 12 bits stored @@ -66,7 +66,7 @@ self.ds.BitsAllocated) -class TimeGetPixelData(object): +class TimeGetPixelData: """Time tests for rle_handler.get_pixeldata.""" def setup(self): """Setup the test""" diff -Nru pydicom-1.4.1/pydicom/benchmarks/bench_handler_rle_encode.py pydicom-2.0.0/pydicom/benchmarks/bench_handler_rle_encode.py --- pydicom-1.4.1/pydicom/benchmarks/bench_handler_rle_encode.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/benchmarks/bench_handler_rle_encode.py 2020-05-29 01:44:31.000000000 +0000 @@ -24,7 +24,7 @@ EXPL_32_3_1F = get_testdata_files("SC_rgb_32bit.dcm")[0] -class TimeRLEEncodeSegment(object): +class TimeRLEEncodeSegment: """Time tests for rle_handler._rle_encode_segment.""" def setup(self): ds = dcmread(EXPL_8_1_1F) @@ -39,7 +39,7 @@ _rle_encode_segment(self.arr) -class TimeRLEEncodeFrame(object): +class TimeRLEEncodeFrame: """Time tests for rle_handler.rle_encode_frame.""" def setup(self): ds = dcmread(EXPL_8_1_1F) diff -Nru pydicom-1.4.1/pydicom/benchmarks/bench_pixel_util.py pydicom-2.0.0/pydicom/benchmarks/bench_pixel_util.py --- pydicom-1.4.1/pydicom/benchmarks/bench_pixel_util.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/benchmarks/bench_pixel_util.py 2020-05-29 01:44:31.000000000 +0000 @@ -6,7 +6,7 @@ from pydicom.pixel_data_handlers.util import convert_color_space -class TimeConvertColorSpace(object): +class TimeConvertColorSpace: """Benchmarks for utils.convert_color_space().""" def setup(self): """Setup the benchmark.""" diff -Nru pydicom-1.4.1/pydicom/charset.py pydicom-2.0.0/pydicom/charset.py --- pydicom-1.4.1/pydicom/charset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/charset.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,9 +4,8 @@ import re import warnings -from pydicom import compat, config -from pydicom.compat import in_py2 -from pydicom.valuerep import PersonNameUnicode, text_VRs, TEXT_VR_DELIMS +from pydicom import config +from pydicom.valuerep import text_VRs, TEXT_VR_DELIMS # default encoding if no encoding defined - corresponds to ISO IR 6 / ASCII default_encoding = "iso8859" @@ -20,8 +19,6 @@ # alias for latin_1 too (iso_ir_6 exists as an alias to 'ascii') 'ISO_IR 6': default_encoding, 'ISO_IR 13': 'shift_jis', - - # these also have iso_ir_1XX aliases in python 2.7 'ISO_IR 100': 'latin_1', 'ISO_IR 101': 'iso8859_2', 'ISO_IR 109': 'iso8859_3', @@ -244,10 +241,9 @@ ---------- encoding : str An encoding is used to specify an escape sequence. - encoded : bytes or str - The encoded value is used to chose an escape sequence if encoding is - 'shift_jis'. Should be :class:`bytes` for Python 3 and :class:`str` - for Python 2. + encoded : bytes + The encoded value is used to choose an escape sequence if encoding is + 'shift_jis'. Returns ------- @@ -262,10 +258,7 @@ if encoded is None: return ESC_ISO_IR_14 - if not in_py2: - first_byte = encoded[0] - else: - first_byte = ord(encoded[0]) + first_byte = encoded[0] if 0x80 <= first_byte: return ESC_ISO_IR_13 @@ -291,25 +284,24 @@ Parameters ---------- - value : bytes or str + value : bytes The encoded byte string in the DICOM element value. Should be - :class:`bytes` for Python 3 and :class:`str` for Python 2. + :class:`bytes` encodings : list of str The encodings needed to decode the string as a list of Python encodings, converted from the encodings in (0008,0005) *Specific Character Set*. - delimiters : set of int (Python 3) or characters (Python 2) + delimiters : set of int A set of characters or character codes, each of which resets the encoding in `value`. Returns ------- - str or unicode + str The decoded unicode string. If the value could not be decoded, and :func:`enforce_valid_values` is ``False``, a warning is issued, and `value` is decoded using the first encoding with replacement characters, resulting in data loss. - Returns :class:`str` for Python 3 and :class:`unicode` for Python 2. Raises ------ @@ -371,7 +363,7 @@ encodings: list of str The list of Python encodings as converted from the values in the Specific Character Set tag. - delimiters: set of int (Python 3) or characters (Python 2) + delimiters: set of int A set of characters or character codes, each of which resets the encoding in `byte_str`. @@ -456,9 +448,8 @@ Parameters ---------- - value : str or unicode - The unicode string as presented to the user. Should be :class:`str` - for Python 3 and :class:`unicode` for Python 2. + value : str + The unicode string as presented to the user. encodings : list of str The encodings needed to encode the string as a list of Python encodings, converted from the encodings in (0008,0005) *Specific @@ -466,13 +457,12 @@ Returns ------- - bytes or str + bytes The encoded string. If `value` could not be encoded with any of the given encodings, and :func:`enforce_valid_values` is ``False``, a warning is issued, and `value` is encoded using the first - encoding with replacement characters, resulting in data loss. Should - be :class:`bytes` for Python 3 and :class:`str` for Python 2. + encoding with replacement characters, resulting in data loss. Raises ------ @@ -647,7 +637,7 @@ # If a list if passed, we don't want to modify the list in place so copy it encodings = encodings[:] - if isinstance(encodings, compat.string_types): + if isinstance(encodings, str): encodings = [encodings] elif not encodings[0]: encodings[0] = 'ISO_IR 6' @@ -768,26 +758,17 @@ # decode the string value to unicode # PN is special case as may have 3 components with different chr sets if data_element.VR == "PN": - if not in_py2: - if data_element.VM <= 1: - data_element.value = data_element.value.decode(encodings) - else: - data_element.value = [ - val.decode(encodings) for val in data_element.value - ] + if data_element.VM <= 1: + data_element.value = data_element.value.decode(encodings) else: - if data_element.VM <= 1: - data_element.value = PersonNameUnicode(data_element.value, - encodings) - else: - data_element.value = [ - PersonNameUnicode(value, encodings) - for value in data_element.value - ] + data_element.value = [ + val.decode(encodings) for val in data_element.value + ] + if data_element.VR in text_VRs: # You can't re-decode unicode (string literals in py3) if data_element.VM == 1: - if isinstance(data_element.value, compat.text_type): + if isinstance(data_element.value, str): return data_element.value = decode_string(data_element.value, encodings, TEXT_VR_DELIMS) @@ -796,7 +777,7 @@ output = list() for value in data_element.value: - if isinstance(value, compat.text_type): + if isinstance(value, str): output.append(value) else: output.append(decode_string(value, encodings, diff -Nru pydicom-1.4.1/pydicom/compat.py pydicom-2.0.0/pydicom/compat.py --- pydicom-1.4.1/pydicom/compat.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/compat.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,34 +1,10 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. -"""Compatibility functions for python 2 vs later versions""" +"""Compatibility functions for previous Python 2 support""" -# These are largely modeled on Armin Ronacher's porting advice -# at http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ - -import sys - -in_py2 = sys.version_info[0] == 2 -in_PyPy = 'PyPy' in sys.version # Text types -# In py3+, the native text type ('str') is unicode -# In py2, str can be either bytes or text. -if in_py2: - text_type = unicode - string_types = (str, unicode) - char_types = (str, unicode) - number_types = (int, long) - int_type = long -else: - text_type = str - string_types = (str, ) - char_types = (str, bytes) - number_types = (int, ) - int_type = int - -if in_py2: - # Have to run through exec as the code is a syntax error in py 3 - exec('def reraise(tp, value, tb):\n raise tp, value, tb') -else: - - def reraise(tp, value, tb): - raise value.with_traceback(tb) +text_type = str +string_types = (str, ) +char_types = (str, bytes) +number_types = (int, ) +int_type = int diff -Nru pydicom-1.4.1/pydicom/config.py pydicom-2.0.0/pydicom/config.py --- pydicom-1.4.1/pydicom/config.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/config.py 2020-05-29 01:44:31.000000000 +0000 @@ -5,11 +5,19 @@ import logging + +have_numpy = True +try: + import numpy +except ImportError: + have_numpy = False + + # Set the type used to hold DS values # default False; was decimal-based in pydicom 0.9.7 use_DS_decimal = False -"""Set to ``True`` to use :class:`decimal.Decimal` to hold the value for -elements with a VR of 'DS'. +"""Set using :func:`~pydicom.config.DS_decimal` to control if elements with a +VR of **DS** are represented as :class:`~decimal.Decimal`. Default ``False``. """ @@ -38,20 +46,63 @@ data_element_callback_kwargs = {} +def DS_numpy(use_numpy=True): + """Set whether multi-valued elements with VR of **DS** will be numpy arrays + + .. versionadded:: 2.0 + + Parameters + ---------- + use_numpy : bool, optional + ``True`` (default) to read multi-value **DS** elements + as :class:`~numpy.ndarray`, ``False`` to read multi-valued **DS** + data elements as type :class:`~python.mulitval.MultiValue` + + Note: once a value has been accessed, changing this setting will + no longer change its type + + Raises + ------ + ValueError + If :data:`use_DS_decimal` and `use_numpy` are both True. + + """ + + global use_DS_numpy + + if use_DS_decimal and use_numpy: + raise ValueError("Cannot use numpy arrays to read DS elements" + "if `use_DS_decimal` is True") + use_DS_numpy = use_numpy + + def DS_decimal(use_Decimal_boolean=True): """Set DS class to be derived from :class:`decimal.Decimal` or - class:`float`. + :class:`float`. If this function is never called, the default in *pydicom* >= 0.9.8 is for DS to be based on :class:`float`. Parameters ---------- - use_Decimal_boolean : bool - ``True`` to derive :class:`~pydicom.valuerep.DS` from + use_Decimal_boolean : bool, optional + ``True`` (default) to derive :class:`~pydicom.valuerep.DS` from :class:`decimal.Decimal`, ``False`` to derive it from :class:`float`. + + Raises + ------ + ValueError + If `use_Decimal_boolean` and :data:`use_DS_numpy` are + both ``True``. """ + global use_DS_decimal + use_DS_decimal = use_Decimal_boolean + + if use_DS_decimal and use_DS_numpy: + raise ValueError("Cannot set use_DS_decimal True " + "if use_DS_numpy is True") + import pydicom.valuerep if use_DS_decimal: pydicom.valuerep.DSclass = pydicom.valuerep.DSdecimal @@ -60,6 +111,21 @@ # Configuration flags +use_DS_numpy = False +"""Set using the function :func:`~pydicom.config.DS_numpy` to control +whether arrays of VR **DS** are returned as numpy arrays. +Default: ``False``. + +.. versionadded:: 2.0 +""" + +use_IS_numpy = False +"""Set to False to avoid IS values being returned as numpy ndarray objects. +Default: ``False``. + +.. versionadded:: 2.0 +""" + allow_DS_float = False """Set to ``True`` to allow :class:`~pydicom.valuerep.DSdecimal` instances to be created using :class:`floats`; otherwise, they must be @@ -90,12 +156,30 @@ """ If ``True``, the value of a decoded empty data element with a text VR is ``None``, otherwise (the default), it is is an empty string. For all other VRs the behavior does not change - the value is en empty -list for VR 'SQ' and ``None`` for all other VRs. +list for VR **SQ** and ``None`` for all other VRs. Note that the default of this value will change to ``True`` in version 2.0. .. versionadded:: 1.4 """ +replace_un_with_known_vr = True +""" If ``True``, and the VR of a known data element is encoded as **UN** in +an explicit encoding, the VR is changed to the known value. +Can be set to ``False`` where the content of the tag shown as **UN** is +not DICOM conformant and would lead to a failure if accessing it. + +.. versionadded:: 2.0 +""" + +show_file_meta = True +""" +.. versionadded:: 2.0 + +If ``True`` (default), the 'str' and 'repr' methods +of :class:`~pydicom.dataset.Dataset` begin with a separate section +displaying the file meta information data elements +""" + # Logging system and debug function to change logging level logger = logging.getLogger('pydicom') logger.addHandler(logging.NullHandler()) diff -Nru pydicom-1.4.1/pydicom/data/data_manager.py pydicom-2.0.0/pydicom/data/data_manager.py --- pydicom-1.4.1/pydicom/data/data_manager.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/data/data_manager.py 2020-05-29 01:44:31.000000000 +0000 @@ -5,6 +5,8 @@ import os from os.path import abspath, dirname, join +from pydicom.fileutil import path_from_pathlike + DATA_ROOT = abspath(dirname(__file__)) @@ -13,7 +15,7 @@ Parameters ---------- - base : str + base : str or PathLike Base directory to recursively search. pattern : str @@ -26,6 +28,7 @@ The list of filenames matched. """ + base = path_from_pathlike(base) # if the user forgot to add them pattern = "*" + pattern + "*" Binary files /tmp/tmpCEx2xZ/E3gW5F3iAx/pydicom-1.4.1/pydicom/data/test_files/bad_sequence.dcm and /tmp/tmpCEx2xZ/UK2CWB7Sxf/pydicom-2.0.0/pydicom/data/test_files/bad_sequence.dcm differ Binary files /tmp/tmpCEx2xZ/E3gW5F3iAx/pydicom-1.4.1/pydicom/data/test_files/empty_charset_LEI.dcm and /tmp/tmpCEx2xZ/UK2CWB7Sxf/pydicom-2.0.0/pydicom/data/test_files/empty_charset_LEI.dcm differ Binary files /tmp/tmpCEx2xZ/E3gW5F3iAx/pydicom-1.4.1/pydicom/data/test_files/JPGLosslessP14SV1_1s_1f_8b.dcm and /tmp/tmpCEx2xZ/UK2CWB7Sxf/pydicom-2.0.0/pydicom/data/test_files/JPGLosslessP14SV1_1s_1f_8b.dcm differ diff -Nru pydicom-1.4.1/pydicom/data/test_files/README.txt pydicom-2.0.0/pydicom/data/test_files/README.txt --- pydicom-1.4.1/pydicom/data/test_files/README.txt 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/data/test_files/README.txt 2020-05-29 01:44:31.000000000 +0000 @@ -41,6 +41,12 @@ * JPEG2000, JPEG2000Lossless and uncompressed versions * Mismatch between BitsStored and sample bit depth +bad_sequence.dcm + * Anonymized test dataset for issue #1067, provided by @sylvainKritter + * JPEGLossless:Non-hierarchical-1stOrderPrediction + * contains invalid sequence (encoded as Implicit Little Endian) with VR + "UN" + CT_small.dcm * CT image, Explicit VR, LittleEndian * Downsized to 128x128 from 'CT1_UNC', ftp://medical.nema.org/MEDICAL/Dicom/DataSets/WG04/ @@ -67,6 +73,10 @@ * unsigned 16-bit/12-bit with rescale and windowing * From ftp://medical.nema.org/MEDICAL/Dicom/DataSets/WG04 +JPGLosslessP14SV1_1s_1f_8b.dcm + * 1.2.840.10008.1.2.4.70 - JPEG Lossless, Process 14, Selection Value 1 + * 1 sample/px, 1 frame, 8-bits stored, monochrome2 + JPEG2000.dcm and JPEG2000_UNC.dcm (uncompressed version) * JPEG 2000 small image * to test JPEG transfer syntax, eventually JPEG decompression @@ -110,7 +120,7 @@ * Modality LUT Sequence * One of the IHE (https://wiki.ihe.net/index.php/Main_Page) MESA display test images - + no_meta.dcm * Same as CT_small.dcm with no File Meta Information header @@ -127,6 +137,10 @@ * from http://www.dclunie.com/images/charset/SCS* * downsized to 32x32 since pixel data is irrelevant for these (test pattern only) +empty_charset_LEI.dcm + * Dataset with empty Specific Character Set, regression dataset for #1038 + * provided by @micjuel + test-SR.dcm * from ftp://ftp.dcmtk.org/pub/dicom/offis/software/dscope/dscope360/support/srdoc103.zip, file "test.dcm" * Structured Reporting example, many levels of nesting diff -Nru pydicom-1.4.1/pydicom/dataelem.py pydicom-2.0.0/pydicom/dataelem.py --- pydicom-1.4.1/pydicom/dataelem.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/dataelem.py 2020-05-29 01:44:31.000000000 +0000 @@ -7,7 +7,6 @@ and a value. """ -from __future__ import absolute_import import base64 import json @@ -15,10 +14,8 @@ from collections import namedtuple from pydicom import config # don't import datetime_conversion directly -from pydicom import compat -from pydicom.charset import default_encoding -from pydicom.compat import in_py2 from pydicom.config import logger +from pydicom import config from pydicom.datadict import (dictionary_has_tag, dictionary_description, dictionary_keyword, dictionary_is_retired, private_dictionary_description, dictionary_VR, @@ -29,13 +26,10 @@ from pydicom.uid import UID from pydicom import jsonrep import pydicom.valuerep # don't import DS directly as can be changed by config +from pydicom.valuerep import PersonName -from pydicom.valuerep import PersonNameUnicode - -if not in_py2: - from pydicom.valuerep import PersonName3 as PersonNameUnicode - -PersonName = PersonNameUnicode +if config.have_numpy: + import numpy BINARY_VR_VALUES = [ 'US', 'SS', 'UL', 'SL', 'OW', 'OB', 'OL', 'UN', @@ -85,27 +79,9 @@ return None -def isMultiValue(value): - """Return ``True`` if `value` is list-like (iterable). - - .. deprecated:: 1.3 - This function is deprecated, use :attr:`DataElement.VM` instead. - - """ - msg = 'isMultiValue is deprecated, use DataElement.VM instead' - warnings.warn(msg, DeprecationWarning) - if isinstance(value, compat.char_types): - return False - try: - iter(value) - except TypeError: - return False - return True - - def _is_bytes(val): - """Return True only in Python 3 if `val` is of type `bytes`.""" - return False if in_py2 else isinstance(val, bytes) + """Return True only if `val` is of type `bytes`.""" + return isinstance(val, bytes) # double '\' because it is used as escape chr in Python @@ -113,7 +89,7 @@ _backslash_byte = b"\\" -class DataElement(object): +class DataElement: """Contain and manipulate a DICOM Element. Examples @@ -153,30 +129,13 @@ descripWidth : int For string display, this is the maximum width of the description field (default ``35``). - is_retired : bool - For officially registered DICOM Data Elements this will be ``True`` if - the retired status as given in the DICOM Standard, Part 6, - :dcm:`Table 6-1` is 'RET'. For private - or unknown elements this will always be ``False``. is_undefined_length : bool Indicates whether the length field for the element was ``0xFFFFFFFFL`` (ie undefined). - keyword : str - For officially registered DICOM Data Elements this will be the - *Keyword* as given in - :dcm:`Table 6-1`. For private or - unknown elements this will return an empty string ``''``. maxBytesToDisplay : int For string display, elements with values containing data which is longer than this value will display ``"array of # bytes"`` (default ``16``). - name : str - For officially registered DICOM Data Elements this will be the *Name* - as given in :dcm:`Table 6-1`. - For private elements known to *pydicom* - this will be the *Name* in the format ``'[name]'``. For unknown - private elements this will be ``'Private Creator'``. For unknown - elements this will return an empty string ``''``. showVR : bool For string display, include the element's VR just before it's value (default ``True``). @@ -184,8 +143,6 @@ The element's tag. value The element's stored value(s). - VM : int - The Value Multiplicity of the element's stored value(s). VR : str The element's Value Representation. """ @@ -195,10 +152,6 @@ showVR = True is_raw = False - # Python 2: Classes which define __eq__ - # should flag themselves as unhashable - __hash__ = None - def __init__(self, tag, VR, @@ -243,8 +196,9 @@ # a known tag shall only have the VR 'UN' if it has a length that # exceeds the size that can be encoded in 16 bit - all other cases # can be seen as an encoding error and can be corrected - if VR == 'UN' and (is_undefined_length or value is None or - len(value) < 0xffff): + if (VR == 'UN' and not tag.is_private and + config.replace_un_with_known_vr and + (is_undefined_length or value is None or len(value) < 0xffff)): try: VR = dictionary_VR(tag) except KeyError: @@ -257,6 +211,7 @@ self.value = value # calls property setter which will convert self.file_tell = file_value_tell self.is_undefined_length = is_undefined_length + self.private_creator = None @classmethod def from_json(cls, dataset_class, tag, vr, value, value_key, @@ -359,8 +314,6 @@ else: value = [self.value] for v in value: - if compat.in_py2: - v = PersonNameUnicode(v, 'UTF8') comps = {'Alphabetic': v.components[0]} if len(v.components) > 1: comps['Ideographic'] = v.components[1] @@ -437,7 +390,7 @@ # Check if is a string with multiple values separated by '\' # If so, turn them into a list of separate strings # Last condition covers 'US or SS' etc - if isinstance(val, compat.char_types) and self.VR not in \ + if isinstance(val, (str, bytes)) and self.VR not in \ ['UT', 'ST', 'LT', 'FL', 'FD', 'AT', 'OB', 'OW', 'OF', 'SL', 'SQ', 'SS', 'UL', 'OB/OW', 'OW/OB', 'OB or OW', 'OW or OB', 'UN'] and 'US' not in self.VR: @@ -454,7 +407,7 @@ """Return the value multiplicity of the element as :class:`int`.""" if self.value is None: return 0 - if isinstance(self.value, (compat.char_types, PersonName)): + if isinstance(self.value, (str, bytes, PersonName)): return 1 if self.value else 0 try: iter(self.value) @@ -536,7 +489,7 @@ return pydicom.valuerep.TM(val) elif self.VR == "UI": return UID(val) if val is not None else None - elif not in_py2 and self.VR == "PN": + elif self.VR == "PN": return PersonName(val) # Later may need this for PersonName as for UI, # but needs more thought @@ -568,8 +521,15 @@ return True if isinstance(other, self.__class__): - return (self.tag == other.tag and self.VR == other.VR - and self.value == other.value) + if self.tag != other.tag or self.VR != other.VR: + return False + + # tag and VR match, now check the value + if config.have_numpy and isinstance(self.value, numpy.ndarray): + return (len(self.value) == len(other.value) + and numpy.allclose(self.value, other.value)) + else: + return self.value == other.value return NotImplemented @@ -611,15 +571,15 @@ def __unicode__(self): """Return unicode representation of the element.""" - if isinstance(self.value, compat.text_type): + if isinstance(self.value, str): # start with the string rep then replace the value part # with the unicode strVal = str(self) strVal = strVal.replace(self.repval, "") - uniVal = compat.text_type(strVal) + self.value + uniVal = str(strVal) + self.value return uniVal else: - return compat.text_type(str(self)) + return str(self) def __getitem__(self, key): """Return the item at `key` if the element's value is indexable.""" @@ -631,14 +591,22 @@ @property def name(self): - """Return the DICOM dictionary name for the element as :class:`str`.""" + """Return the DICOM dictionary name for the element as :class:`str`. + + For officially registered DICOM Data Elements this will be the *Name* + as given in :dcm:`Table 6-1`. + For private elements known to *pydicom* + this will be the *Name* in the format ``'[name]'``. For unknown + private elements this will be ``'Private Creator'``. For unknown + elements this will return an empty string ``''``. + """ return self.description() def description(self): """Return the DICOM dictionary name for the element as :class:`str`.""" if self.tag.is_private: name = "Private tag data" # default - if hasattr(self, 'private_creator'): + if self.private_creator: try: # If have name from private dictionary, use it, but # but put in square brackets so is differentiated, @@ -648,7 +616,7 @@ name = "[%s]" % (name) except KeyError: pass - elif self.tag.elem >> 8 == 0: + elif self.tag.element >> 8 == 0: name = "Private Creator" elif dictionary_has_tag(self.tag) or repeater_has_tag(self.tag): name = dictionary_description(self.tag) @@ -662,7 +630,13 @@ @property def is_retired(self): - """Return the element's retired status as :class:`bool`.""" + """Return the element's retired status as :class:`bool`. + + For officially registered DICOM Data Elements this will be ``True`` if + the retired status as given in the DICOM Standard, Part 6, + :dcm:`Table 6-1` is 'RET'. For private + or unknown elements this will always be ``False``. + """ if dictionary_has_tag(self.tag): return dictionary_is_retired(self.tag) else: @@ -670,7 +644,13 @@ @property def keyword(self): - """Return the element's keyword (if known) as :class:`str`.""" + """Return the element's keyword (if known) as :class:`str`. + + For officially registered DICOM Data Elements this will be the + *Keyword* as given in + :dcm:`Table 6-1`. For private or + unknown elements this will return an empty string ``''``. + """ if dictionary_has_tag(self.tag): return dictionary_keyword(self.tag) else: @@ -714,8 +694,6 @@ # filereader->Dataset->convert_value->filereader # (for SQ parsing) - if in_py2: - encoding = encoding or default_encoding from pydicom.values import convert_value raw = raw_data_element @@ -744,7 +722,8 @@ msg = "Unknown DICOM tag {0:s}".format(str(raw.tag)) msg += " can't look up VR" raise KeyError(msg) - elif VR == 'UN' and not raw.tag.is_private: + elif (VR == 'UN' and not raw.tag.is_private and + config.replace_un_with_known_vr): # handle rare case of incorrectly set 'UN' in explicit encoding # see also DataElement.__init__() if (raw.length == 0xffffffff or raw.value is None or @@ -758,9 +737,13 @@ except NotImplementedError as e: raise NotImplementedError("{0:s} in tag {1!r}".format(str(e), raw.tag)) - if raw.tag in _LUT_DESCRIPTOR_TAGS and value[0] < 0: + if raw.tag in _LUT_DESCRIPTOR_TAGS and value: # We only fix the first value as the third value is 8 or 16 - value[0] += 65536 + try: + if value[0] < 0: + value[0] += 65536 + except TypeError: + pass return DataElement(raw.tag, VR, value, raw.value_tell, raw.length == 0xFFFFFFFF, already_converted=True) diff -Nru pydicom-1.4.1/pydicom/dataset.py pydicom-2.0.0/pydicom/dataset.py --- pydicom-1.4.1/pydicom/dataset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/dataset.py 2020-05-29 01:44:31.000000000 +0000 @@ -27,7 +27,7 @@ import pydicom # for dcmwrite import pydicom.charset import pydicom.config -from pydicom import compat, datadict, jsonrep +from pydicom import datadict, jsonrep from pydicom._version import __version_info__ from pydicom.charset import default_encoding, convert_encodings from pydicom.config import logger @@ -35,6 +35,7 @@ from pydicom.datadict import (tag_for_keyword, keyword_for_tag, repeater_has_keyword) from pydicom.dataelem import DataElement, DataElement_from_raw, RawDataElement +from pydicom.fileutil import path_from_pathlike from pydicom.pixel_data_handlers.util import ( convert_color_space, reshape_pixel_array, get_image_pixel_ids ) @@ -43,10 +44,7 @@ ExplicitVRBigEndian, PYDICOM_IMPLEMENTATION_UID) -if compat.in_py2: - from pkgutil import find_loader as have_package -else: - from importlib.util import find_spec as have_package +from importlib.util import find_spec as have_package have_numpy = True try: @@ -55,7 +53,7 @@ have_numpy = False -class PrivateBlock(object): +class PrivateBlock: """Helper class for a private block in the :class:`Dataset`. .. versionadded:: 1.3 @@ -183,7 +181,9 @@ The value of the data element. See :meth:`Dataset.add_new()` for a description. """ - self.dataset.add_new(self.get_tag(element_offset), VR, value) + tag = self.get_tag(element_offset) + self.dataset.add_new(tag, VR, value) + self.dataset[tag].private_creator = self.private_creator def _dict_equal(a, b, exclude=None): @@ -352,9 +352,6 @@ """ indent_chars = " " - # Python 2: Classes defining __eq__ should flag themselves as unhashable - __hash__ = None - def __init__(self, *args, **kwargs): """Create a new :class:`Dataset` instance.""" self._parent_encoding = kwargs.get('parent_encoding', default_encoding) @@ -585,14 +582,22 @@ if isinstance(key, slice): for tag in self._slice_dataset(key.start, key.stop, key.step): del self._dict[tag] + # invalidate private blocks in case a private creator is + # deleted - will be re-created on next access + if self._private_blocks and BaseTag(tag).is_private_creator: + self._private_blocks = {} else: # Assume is a standard tag (for speed in common case) try: del self._dict[key] + if self._private_blocks and BaseTag(key).is_private_creator: + self._private_blocks = {} # If not a standard tag, than convert to Tag and try again except KeyError: tag = Tag(key) del self._dict[tag] + if self._private_blocks and tag.is_private_creator: + self._private_blocks = {} def __dir__(self): """Give a list of attributes available in the :class:`Dataset`. @@ -671,7 +676,7 @@ Parameters ---------- - key : str or int or BaseTag + key : str or int or Tuple[int, int] or BaseTag The element keyword or tag or the class attribute name to get. default : obj or None, optional If the element or class attribute is not present, return @@ -689,7 +694,7 @@ value If `key` is a class attribute then return its value. """ - if isinstance(key, (str, compat.text_type)): + if isinstance(key, str): try: return getattr(self, key) except AttributeError: @@ -741,16 +746,6 @@ """ return self._dict.values() - if compat.in_py2: - def iterkeys(self): - return self._dict.iterkeys() - - def itervalues(self): - return self._dict.itervalues() - - def iteritems(self): - return self._dict.iteritems() - def __getattr__(self, name): """Intercept requests for :class:`Dataset` attribute names. @@ -790,7 +785,7 @@ if not char_set: char_set = self._parent_encoding else: - char_set = convert_encodings(char_set) + char_set = convert_encodings(char_set.value) return char_set @@ -907,9 +902,8 @@ Returns ------- - int - Element base for the given tag (the last 2 hex digits are always 0) - as a 32-bit :class:`int`. + PrivateBlock + The existing or newly created private block. Raises ------ @@ -920,7 +914,7 @@ If the private creator tag is not found in the given group and the `create` parameter is ``False``. """ - def new_block(): + def new_block(element): block = PrivateBlock(key, self, element) self._private_blocks[key] = block return block @@ -936,19 +930,23 @@ raise ValueError( 'Tag must be private if private creator is given') - for element in range(0x10, 0x100): - private_creator_tag = Tag(group, element) - if private_creator_tag not in self._dict: - if create: - self.add_new(private_creator_tag, 'LO', private_creator) - return new_block() - else: - break - if self._dict[private_creator_tag].value == private_creator: - return new_block() - - raise KeyError( - "Private creator '{}' not found".format(private_creator)) + # find block with matching private creator + data_el = next((el for el in self[(group, 0x10):(group, 0x100)] + if el.value == private_creator), None) + if data_el is not None: + return new_block(data_el.tag.element) + + if not create: + # not found and shall not be created - raise + raise KeyError( + "Private creator '{}' not found".format(private_creator)) + + # private creator not existing - find first unused private block + # and add the private creator + first_free_el = next(el for el in range(0x10, 0x100) + if Tag(group, el) not in self._dict) + self.add_new(Tag(group, first_free_el), 'LO', private_creator) + return new_block(first_free_el) def private_creators(self, group): """Return a list of private creator names in the given group. @@ -982,13 +980,7 @@ if group % 2 == 0: raise ValueError('Group must be an odd number') - private_creators = [] - for element in range(0x10, 0x100): - private_creator_tag = Tag(group, element) - if private_creator_tag not in self._dict: - break - private_creators.append(self._dict[private_creator_tag].value) - return private_creators + return [x.value for x in self[(group, 0x10):(group, 0x100)]] def get_private_item(self, group, element_offset, private_creator): """Return the data element for the given private tag `group`. @@ -1258,7 +1250,7 @@ Returns ------- - type + DataElement or type The data element for `key` if it exists, or the default value if it is a :class:`~pydicom.dataelem.DataElement` or ``None``, or a :class:`~pydicom.dataelem.DataElement` @@ -1682,6 +1674,11 @@ It is also used by ``top()``, therefore the `top_level_only` flag. This function recurses, with increasing indentation levels. + ..versionchanged:: 2.0 + + The file meta information is returned in its own section, + if :data:`~pydicom.config.show_file_meta` is ``True`` (default) + Parameters ---------- indent : int, optional @@ -1698,6 +1695,20 @@ strings = [] indent_str = self.indent_chars * indent nextindent_str = self.indent_chars * (indent + 1) + + # Display file meta, if configured to do so, and have a non-empty one + if ( + hasattr(self, "file_meta") + and self.file_meta is not None + and len(self.file_meta) > 0 + and pydicom.config.show_file_meta + ): + strings.append("Dataset.file_meta -------------------------------") + for data_element in self.file_meta: + with tag_in_exception(data_element.tag): + strings.append(indent_str + repr(data_element)) + strings.append("-------------------------------------------------") + for data_element in self: with tag_in_exception(data_element.tag): if data_element.VR == "SQ": # a sequence @@ -1727,86 +1738,14 @@ def save_as(self, filename, write_like_original=True): """Write the :class:`Dataset` to `filename`. - Saving requires that the ``Dataset.is_implicit_VR`` and - ``Dataset.is_little_endian`` attributes exist and are set - appropriately. If ``Dataset.file_meta.TransferSyntaxUID`` is present - then it should be set to a consistent value to ensure conformance. - - **Conformance with DICOM File Format** - - If `write_like_original` is ``False``, the :class:`Dataset` will be - stored in the :dcm:`DICOM File Format `. To do - so requires that the ``Dataset.file_meta`` attribute - exists and contains a :class:`Dataset` with the required (Type 1) *File - Meta Information Group* elements (see - :func:`~pydicom.filewriter.dcmwrite` and - :func:`~pydicom.filewriter.write_file_meta_info` for more information). - - If `write_like_original` is ``True`` then the :class:`Dataset` will be - written as is (after minimal validation checking) and may or may not - contain all or parts of the *File Meta Information* (and hence may or - may not be conformant with the DICOM File Format). - - Parameters - ---------- - filename : str or file-like - Name of file or the file-like to write the new DICOM file to. - write_like_original : bool, optional - If ``True`` (default), preserves the following information from - the :class:`Dataset` (and may result in a non-conformant file): - - - preamble -- if the original file has no preamble then none will - be written. - - file_meta -- if the original file was missing any required *File - Meta Information Group* elements then they will not be added or - written. - If (0002,0000) *File Meta Information Group Length* is present - then it may have its value updated. - - seq.is_undefined_length -- if original had delimiters, write them - now too, instead of the more sensible length characters - - is_undefined_length_sequence_item -- for datasets that belong to - a sequence, write the undefined length delimiters if that is - what the original had. - - If ``False``, produces a file conformant with the DICOM File - Format, with explicit lengths for all elements. + Wrapper for pydicom.filewriter.dcmwrite, passing this dataset to it. + See documentation for that function for details. See Also -------- - pydicom.filewriter.write_dataset - Write a :class:`Dataset` to a file. - pydicom.filewriter.write_file_meta_info - Write the *File Meta Information Group* elements to a file. pydicom.filewriter.dcmwrite Write a DICOM file from a :class:`FileDataset` instance. """ - # Ensure is_little_endian and is_implicit_VR are set - if None in (self.is_little_endian, self.is_implicit_VR): - has_tsyntax = False - try: - tsyntax = self.file_meta.TransferSyntaxUID - if not tsyntax.is_private: - self.is_little_endian = tsyntax.is_little_endian - self.is_implicit_VR = tsyntax.is_implicit_VR - has_tsyntax = True - except AttributeError: - pass - - if not has_tsyntax: - raise AttributeError( - "'{0}.is_little_endian' and '{0}.is_implicit_VR' must be " - "set appropriately before saving." - .format(self.__class__.__name__) - ) - - # Try and ensure that `is_undefined_length` is set correctly - try: - tsyntax = self.file_meta.TransferSyntaxUID - if not tsyntax.is_private: - self['PixelData'].is_undefined_length = tsyntax.is_compressed - except (AttributeError, KeyError): - pass - pydicom.dcmwrite(filename, self, write_like_original) def ensure_file_meta(self): @@ -1814,7 +1753,9 @@ .. versionadded:: 1.2 """ - self.file_meta = getattr(self, 'file_meta', Dataset()) + # Changed in v2.0 so does not re-assign self.file_meta with getattr() + if not hasattr(self, "file_meta"): + self.file_meta = FileMetaDataset() def fix_meta_info(self, enforce_standard=True): """Ensure the file meta info exists and has the correct values @@ -1889,12 +1830,25 @@ 'element and must be added using ' 'the add() or add_new() methods.' .format(name)) + elif name == "file_meta": + self._set_file_meta(value) else: # name not in dicom dictionary - setting a non-dicom instance # attribute # XXX note if user mis-spells a dicom data_element - no error!!! object.__setattr__(self, name, value) + def _set_file_meta(self, value): + if value is not None and not isinstance(value, FileMetaDataset): + FileMetaDataset.validate(value) + warnings.warn( + "Starting in pydicom 3.0, Dataset.file_meta must be a " + "FileMetaDataset class instance", + DeprecationWarning + ) + + self.__dict__["file_meta"] = value + def __setitem__(self, key, value): """Operator for Dataset[key] = value. @@ -1902,7 +1856,7 @@ Parameters ---------- - key : int + key : int or Tuple[int, int] or str The tag for the element to be added to the Dataset. value : dataelem.DataElement or dataelem.RawDataElement The element to add to the :class:`Dataset`. @@ -1993,7 +1947,14 @@ return all_tags[i_start:i_stop:step] def __str__(self): - """Handle str(dataset).""" + """Handle str(dataset). + + ..versionchanged:: 2.0 + + The file meta information was added in its own section, + if :data:`pydicom.config.show_file_meta` is ``True`` + + """ return self._pretty_str() def top(self): @@ -2018,7 +1979,7 @@ current object. """ for key, value in list(dictionary.items()): - if isinstance(key, (str, compat.text_type)): + if isinstance(key, str): setattr(self, key, value) else: self[Tag(key)] = value @@ -2223,9 +2184,10 @@ preamble : str or bytes or None The optional DICOM preamble prepended to the :class:`FileDataset`, if available. - file_meta : Dataset or None - The Dataset's file meta information as a :class:`Dataset`, if available - (``None`` if not present). Consists of group ``0x0002`` elements. + file_meta : FileMetaDataset or None + The Dataset's file meta information as a :class:`FileMetaDataset`, + if available (``None`` if not present). + Consists of group ``0x0002`` elements. filename : str or None The filename that the :class:`FileDataset` was read from (if read from file) or ``None`` if the filename is not available (if read from a @@ -2254,7 +2216,7 @@ Parameters ---------- - filename_or_obj : str or BytesIO or None + filename_or_obj : str or PathLike or BytesIO or None Full path and filename to the file, memory buffer object, or ``None`` if is a :class:`io.BytesIO`. dataset : Dataset or dict @@ -2279,7 +2241,8 @@ self.is_implicit_VR = is_implicit_VR self.is_little_endian = is_little_endian filename = None - if isinstance(filename_or_obj, compat.string_types): + filename_or_obj = path_from_pathlike(filename_or_obj) + if isinstance(filename_or_obj, str): filename = filename_or_obj self.fileobj_type = open elif isinstance(filename_or_obj, io.BufferedReader): @@ -2393,3 +2356,90 @@ for tag in missing: msg += '\t{0} {1}\n'.format(tag, keyword_for_tag(tag)) raise ValueError(msg[:-1]) # Remove final newline + + +class FileMetaDataset(Dataset): + """Contains a collection (dictionary) of group 2 DICOM Data Elements. + + .. versionadded:: 2.0 + + Derived from :class:`~pydicom.dataset.Dataset`, but only allows + Group 2 (File Meta Information) data elements + """ + + def __init__(self, *args, **kwargs): + """Initialize a FileMetaDataset + + Parameters are as per :class:`Dataset`; this overrides the super class + only to check that all are group 2 data elements + + Raises + ------ + ValueError + If any data elements are not group 2. + TypeError + If the passed argument is not a :class:`dict` or :class:`Dataset` + """ + super().__init__(*args, **kwargs) + FileMetaDataset.validate(self._dict) + + @staticmethod + def validate(init_value): + """Raise errors if initialization value is not acceptable for file_meta + + Parameters + ---------- + init_value: dict or Dataset + The tag:data element pairs to initialize a file meta dataset + + Raises + ------ + TypeError + If the passed argument is not a :class:`dict` or :class:`Dataset` + ValueError + If any data elements passed are not group 2. + """ + if init_value is None: + return + + if not isinstance(init_value, (Dataset, dict)): + raise TypeError( + "Argument must be a dict or Dataset, not {}".format( + type(init_value) + ) + ) + + non_group2 = [ + Tag(tag) for tag in init_value.keys() if Tag(tag).group != 2 + ] + if non_group2: + msg = "Attempted to set non-group 2 elements: {}" + raise ValueError(msg.format(non_group2)) + + def __setitem__(self, key, value): + """Override parent class to only allow setting of group 2 elements. + + Parameters + ---------- + key : int or Tuple[int, int] or str + The tag for the element to be added to the Dataset. + value : dataelem.DataElement or dataelem.RawDataElement + The element to add to the :class:`FileMetaDataset`. + + Raises + ------ + ValueError + If `key` is not a DICOM Group 2 tag. + """ + + if isinstance(value.tag, BaseTag): + tag = value.tag + else: + tag = Tag(value.tag) + + if tag.group != 2: + raise ValueError( + "Only group 2 data elements are allowed in a FileMetaDataset" + ) + + super().__setitem__(key, value) diff -Nru pydicom-1.4.1/pydicom/_dicom_dict.py pydicom-2.0.0/pydicom/_dicom_dict.py --- pydicom-1.4.1/pydicom/_dicom_dict.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/_dicom_dict.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,5 +1,4 @@ """DICOM data dictionary auto-generated by generate_dicom_dict.py""" -from __future__ import absolute_import # Each dict entry is Tag : (VR, VM, Name, Retired, Keyword) DicomDictionary = { @@ -3071,6 +3070,8 @@ 0x00687001: ('CS', '1', "Model Modification", '', 'ModelModification'), # noqa 0x00687002: ('CS', '1', "Model Mirroring", '', 'ModelMirroring'), # noqa 0x00687003: ('SQ', '1', "Model Usage Code Sequence", '', 'ModelUsageCodeSequence'), # noqa + 0x00687004: ('UI', '1', "Model Group UID", '', 'ModelGroupUID'), # noqa + 0x00687005: ('UR', '1', "Relative URI Reference Within Encapsulated Document", '', 'RelativeURIReferenceWithinEncapsulatedDocument'), # noqa 0x00700001: ('SQ', '1', "Graphic Annotation Sequence", '', 'GraphicAnnotationSequence'), # noqa 0x00700002: ('CS', '1', "Graphic Layer", '', 'GraphicLayer'), # noqa 0x00700003: ('CS', '1', "Bounding Box Annotation Units", '', 'BoundingBoxAnnotationUnits'), # noqa @@ -4599,6 +4600,17 @@ 0x30100087: ('SQ', '1', "Weekday Fraction Pattern Sequence", '', 'WeekdayFractionPatternSequence'), # noqa 0x30100088: ('SQ', '1', "Delivery Time Structure Code Sequence", '', 'DeliveryTimeStructureCodeSequence'), # noqa 0x30100089: ('SQ', '1', "Treatment Site Modifier Code Sequence", '', 'TreatmentSiteModifierCodeSequence'), # noqa + 0x30100090: ('CS', '1', "Robotic Base Location Indicator", '', 'RoboticBaseLocationIndicator'), # noqa + 0x30100091: ('SQ', '1', "Robotic Path Node Set Code Sequence", '', 'RoboticPathNodeSetCodeSequence'), # noqa + 0x30100092: ('UL', '1', "Robotic Node Identifier", '', 'RoboticNodeIdentifier'), # noqa + 0x30100093: ('FD', '3', "RT Treatment Source Coordinates", '', 'RTTreatmentSourceCoordinates'), # noqa + 0x30100094: ('FD', '1', "Radiation Source Coordinate SystemYaw Angle", '', 'RadiationSourceCoordinateSystemYawAngle'), # noqa + 0x30100095: ('FD', '1', "Radiation Source Coordinate SystemRoll Angle", '', 'RadiationSourceCoordinateSystemRollAngle'), # noqa + 0x30100096: ('FD', '1', "Radiation Source Coordinate System Pitch Angle", '', 'RadiationSourceCoordinateSystemPitchAngle'), # noqa + 0x30100097: ('SQ', '1', "Robotic Path Control Point Sequence", '', 'RoboticPathControlPointSequence'), # noqa + 0x30100098: ('SQ', '1', "Tomotherapeutic Control Point Sequence", '', 'TomotherapeuticControlPointSequence'), # noqa + 0x30100099: ('FD', '1-n', "Tomotherapeutic Leaf Open Durations", '', 'TomotherapeuticLeafOpenDurations'), # noqa + 0x3010009A: ('FD', '1-n', "Tomotherapeutic Leaf Initial Closed Durations", '', 'TomotherapeuticLeafInitialClosedDurations'), # noqa 0x40000010: ('LT', '1', "Arbitrary", 'Retired', 'Arbitrary'), # noqa 0x40004000: ('LT', '1', "Text Comments", 'Retired', 'TextComments'), # noqa 0x40080040: ('SH', '1', "Results ID", 'Retired', 'ResultsID'), # noqa diff -Nru pydicom-1.4.1/pydicom/dicomdir.py pydicom-2.0.0/pydicom/dicomdir.py --- pydicom-1.4.1/pydicom/dicomdir.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/dicomdir.py 2020-05-29 01:44:31.000000000 +0000 @@ -28,7 +28,7 @@ Parameters ---------- - filename_or_obj : str or None + filename_or_obj : str or PathLike or file-like or None Full path and filename to the file of ``None`` if :class:`io.BytesIO`. dataset : dataset.Dataset diff -Nru pydicom-1.4.1/pydicom/env_info.py pydicom-2.0.0/pydicom/env_info.py --- pydicom-1.4.1/pydicom/env_info.py 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/pydicom/env_info.py 2020-05-29 01:44:31.000000000 +0000 @@ -0,0 +1,50 @@ +# Copyright 2020 pydicom authors. See LICENSE file for details. +""" +Gather system information and version information for pydicom and auxiliary +modules. + +The output is a GitHub-flavoured markdown table whose contents can help +diagnose any perceived bugs in pydicom. This can be pasted directly into a new +GitHub bug report. + +This file is intended to be run as an executable module. +""" + +import platform +import sys +import importlib + + +def main(): + version_rows = [("platform", platform.platform()), ("Python", sys.version)] + + for module in ("pydicom", "gdcm", "jpeg_ls", "numpy", "PIL"): + try: + m = importlib.import_module(module) + except ImportError: + version = "_module not found_" + else: + version = extract_version(m) or "**cannot determine version**" + + version_rows.append((module, version)) + + print_table(version_rows) + + +def print_table(version_rows): + row_format = "{:12} | {}" + print(row_format.format("module", "version")) + print(row_format.format("------", "-------")) + for module, version in version_rows: + # Some version strings have multiple lines and need to be squashed + print(row_format.format(module, version.replace("\n", " "))) + + +def extract_version(module): + if module.__name__ == "gdcm": + return getattr(module, "GDCM_VERSION", None) + return getattr(module, "__version__", None) + + +if __name__ == "__main__": + main() diff -Nru pydicom-1.4.1/pydicom/filebase.py pydicom-2.0.0/pydicom/filebase.py --- pydicom-1.4.1/pydicom/filebase.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/filebase.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,7 +1,6 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Hold DicomFile class, which does basic I/O for a dicom file.""" -from __future__ import absolute_import from pydicom.tag import Tag, BaseTag from struct import (unpack, pack) @@ -9,7 +8,7 @@ from io import BytesIO -class DicomIO(object): +class DicomIO: """File object which holds transfer syntax info and anything else we need. """ diff -Nru pydicom-1.4.1/pydicom/filereader.py pydicom-2.0.0/pydicom/filereader.py --- pydicom-1.4.1/pydicom/filereader.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/filereader.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,7 +1,6 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Read a dicom media file""" -from __future__ import absolute_import # Need zlib and io.BytesIO for deflate-compressed file from io import BytesIO @@ -10,19 +9,17 @@ import warnings import zlib -from pydicom import compat # don't import datetime_conversion directly from pydicom import config from pydicom.charset import (default_encoding, convert_encodings) -from pydicom.compat import in_py2 from pydicom.config import logger from pydicom.datadict import dictionary_VR, tag_for_keyword from pydicom.dataelem import (DataElement, RawDataElement, DataElement_from_raw, empty_value_for_VR) -from pydicom.dataset import (Dataset, FileDataset) +from pydicom.dataset import (Dataset, FileDataset, FileMetaDataset) from pydicom.dicomdir import DicomDir from pydicom.errors import InvalidDicomError from pydicom.filebase import DicomFile -from pydicom.fileutil import read_undefined_length_value +from pydicom.fileutil import read_undefined_length_value, path_from_pathlike from pydicom.misc import size_in_bytes from pydicom.sequence import Sequence from pydicom.tag import (ItemTag, SequenceDelimiterTag, TupleTag, Tag, BaseTag) @@ -121,7 +118,7 @@ tag_set = set() if specific_tags is not None: for tag in specific_tags: - if isinstance(tag, (str, compat.text_type)): + if isinstance(tag, str): tag = Tag(tag_for_keyword(tag)) if isinstance(tag, BaseTag): tag_set.add(tag) @@ -143,8 +140,7 @@ group, elem, length = element_struct_unpack(bytes_read) else: # explicit VR group, elem, VR, length = element_struct_unpack(bytes_read) - if not in_py2: - VR = VR.decode(default_encoding) + VR = VR.decode(default_encoding) if VR in extra_length_VRs: bytes_read = fp_read(4) length = extra_length_unpack(bytes_read)[0] @@ -205,7 +201,7 @@ # If the tag is (0008,0005) Specific Character Set, then store it if tag == BaseTag(0x00080005): from pydicom.values import convert_string - encoding = convert_string(value, is_little_endian) + encoding = convert_string(value or b'', is_little_endian) # Store the encoding value in the generator # for use with future elements (SQs) encoding = convert_encodings(encoding) @@ -251,15 +247,6 @@ value = read_undefined_length_value(fp, is_little_endian, delimiter, defer_size) - # If the tag is (0008,0005) Specific Character Set, - # then store it - if tag == (0x08, 0x05): - from pydicom.values import convert_string - encoding = convert_string(value, is_little_endian) - # Store the encoding value in the generator for use - # with future elements (SQs) - encoding = convert_encodings(encoding) - # tags with undefined length are skipped after read if has_tag_set and tag not in tag_set: continue @@ -297,9 +284,7 @@ # extremely unlikely that the tag length accidentally has such a # representation - this would need the first tag to be longer than 16kB # (e.g. it should be > 0x4141 = 16705 bytes) - vr1 = ord(vr[0]) if in_py2 else vr[0] - vr2 = ord(vr[1]) if in_py2 else vr[1] - found_implicit = not (0x40 < vr1 < 0x5B and 0x40 < vr2 < 0x5B) + found_implicit = not (0x40 < vr[0] < 0x5B and 0x40 < vr[1] < 0x5B) if found_implicit != implicit_vr_is_assumed: # first check if the tag still belongs to the dataset if stop_when @@ -394,7 +379,7 @@ ds = Dataset(raw_data_elements) if 0x00080005 in raw_data_elements: - char_set = DataElement_from_raw(raw_data_elements[0x00080005]) + char_set = DataElement_from_raw(raw_data_elements[0x00080005]).value encoding = convert_encodings(char_set) else: encoding = parent_encoding @@ -531,8 +516,12 @@ return tag.group != 2 start_file_meta = fp.tell() - file_meta = read_dataset(fp, is_implicit_VR=False, is_little_endian=True, - stop_when=_not_group_0002) + file_meta = FileMetaDataset( + read_dataset( + fp, is_implicit_VR=False, is_little_endian=True, + stop_when=_not_group_0002 + ) + ) if not file_meta._dict: return file_meta @@ -543,9 +532,12 @@ file_meta[list(file_meta.elements())[0].tag] except NotImplementedError: fp.seek(start_file_meta) - file_meta = read_dataset(fp, is_implicit_VR=True, - is_little_endian=True, - stop_when=_not_group_0002) + file_meta = FileMetaDataset( + read_dataset( + fp, is_implicit_VR=True, is_little_endian=True, + stop_when=_not_group_0002 + ) + ) # Log if the Group Length doesn't match actual length if 'FileMetaInformationGroupLength' in file_meta: @@ -706,8 +698,7 @@ # Test the VR to see if it's valid, and if so then assume explicit VR from pydicom.values import converters - if not in_py2: - VR = VR.decode(default_encoding) + VR = VR.decode(default_encoding) if VR in converters.keys(): is_implicit_VR = False # Big endian encoding can only be explicit VR @@ -726,7 +717,7 @@ is_implicit_VR = False is_little_endian = False elif transfer_syntax == pydicom.uid.DeflatedExplicitVRLittleEndian: - # See PS3.6-2008 A.5 (p 71) + # See PS3.5 section A.5 # when written, the entire dataset following # the file metadata was prepared the normal way, # then "deflate" compression applied. @@ -785,7 +776,7 @@ Parameters ---------- - fp : str or file-like + fp : str or PathLike or file-like Either a file-like object, or a string containing the file name. If a file-like object, the caller is responsible for closing it. defer_size : int or str or None, optional @@ -845,7 +836,8 @@ """ # Open file if not already a file object caller_owns_file = True - if isinstance(fp, compat.string_types): + fp = path_from_pathlike(fp) + if isinstance(fp, str): # caller provided a file name; we own the file handle caller_owns_file = False try: @@ -972,7 +964,7 @@ if filename_or_obj is None: raise IOError("Deferred read -- original filename not stored. " "Cannot re-open") - is_filename = isinstance(filename_or_obj, compat.string_types) + is_filename = isinstance(filename_or_obj, str) # Check that the file is the same as when originally read if is_filename and not os.path.exists(filename_or_obj): diff -Nru pydicom-1.4.1/pydicom/fileutil.py pydicom-2.0.0/pydicom/fileutil.py --- pydicom-1.4.1/pydicom/fileutil.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/fileutil.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,6 +1,8 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Functions for reading to certain bytes, e.g. delimiters.""" - +import os +import pathlib +import sys from struct import pack, unpack from pydicom.misc import size_in_bytes @@ -93,7 +95,7 @@ is_little_endian, delimiter_tag, defer_size=None, - read_size=1024*8): + read_size=1024 * 8): """Read until `delimiter_tag` and return the value up to that point. On completion, the file will be set to the first byte after the delimiter @@ -260,3 +262,32 @@ if length != 0: logger.warn("Expected delimiter item to have length 0, " "got %d at file position 0x%x", length, fp.tell() - 4) + + +def path_from_pathlike(file_object): + """Returns the path if `file_object` is a path-like object, otherwise the + original `file_object`. + + Parameters + ---------- + file_object: str or PathLike or file-like + + Returns + ------- + str or file-like + the string representation of the given path object, or the object + itself in case of an object not representing a path. + + ..note: + + ``PathLike`` objects have been introduced in Python 3.6. In Python 3.5, + only objects of type :class:`pathlib.Path` are considered. + """ + if sys.version_info < (3, 6): + if isinstance(file_object, pathlib.Path): + return str(file_object) + return file_object + try: + return os.fspath(file_object) + except TypeError: + return file_object diff -Nru pydicom-1.4.1/pydicom/filewriter.py pydicom-2.0.0/pydicom/filewriter.py --- pydicom-1.4.1/pydicom/filewriter.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/filewriter.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,27 +1,32 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Functions related to writing DICOM data.""" -from __future__ import absolute_import import warnings +import zlib from struct import pack -from pydicom import compat -from pydicom.compat import in_py2 from pydicom.charset import ( default_encoding, text_VRs, convert_encodings, encode_string ) +from pydicom.config import have_numpy from pydicom.dataelem import DataElement_from_raw from pydicom.dataset import Dataset, validate_file_meta from pydicom.filebase import DicomFile, DicomFileLike, DicomBytesIO +from pydicom.fileutil import path_from_pathlike from pydicom.multival import MultiValue from pydicom.tag import (Tag, ItemTag, ItemDelimiterTag, SequenceDelimiterTag, tag_in_exception) -from pydicom.uid import UncompressedPixelTransferSyntaxes -from pydicom.valuerep import extra_length_VRs, PersonNameUnicode +from pydicom.uid import (UncompressedPixelTransferSyntaxes, + DeflatedExplicitVRLittleEndian) +from pydicom.valuerep import extra_length_VRs from pydicom.values import convert_numbers +if have_numpy: + import numpy + + def _correct_ambiguous_vr_element(elem, ds, is_little_endian): """Implementation for `correct_ambiguous_vr_element`. See `correct_ambiguous_vr_element` for description. @@ -259,6 +264,8 @@ def _is_multi_value(val): """Return True if `val` is a multi-value container.""" + if have_numpy and isinstance(val, numpy.ndarray): + return True return isinstance(val, (MultiValue, list, tuple)) @@ -279,14 +286,7 @@ else: val = data_element.value - if val and isinstance(val[0], compat.text_type) or not in_py2: - try: - val = [elem.encode(encodings) for elem in val] - except TypeError: - # we get here in Python 2 if val is a unicode string - val = [PersonNameUnicode(elem, encodings) for elem in val] - val = [elem.encode(encodings) for elem in val] - + val = [elem.encode(encodings) for elem in val] val = b'\\'.join(val) if len(val) % 2 != 0: @@ -301,7 +301,7 @@ if val is not None: if len(val) % 2 != 0: val = val + padding # pad to even length - if isinstance(val, compat.text_type): + if isinstance(val, str): val = val.encode(default_encoding) fp.write(val) @@ -312,13 +312,13 @@ if val is not None: encodings = encodings or [default_encoding] if _is_multi_value(val): - if val and isinstance(val[0], compat.text_type): + if val and isinstance(val[0], str): val = b'\\'.join([encode_string(val, encodings) for val in val]) else: val = b'\\'.join([val for val in val]) else: - if isinstance(val, compat.text_type): + if isinstance(val, str): val = encode_string(val, encodings) if len(val) % 2 != 0: @@ -335,8 +335,8 @@ if _is_multi_value(val): val = "\\".join((x.original_string - if hasattr(x, 'original_string') else str(x) - for x in val)) + if hasattr(x, 'original_string') + else str(x) for x in val)) else: if hasattr(val, 'original_string'): val = val.original_string @@ -346,8 +346,7 @@ if len(val) % 2 != 0: val = val + ' ' # pad to even length - if not in_py2: - val = bytes(val, default_encoding) + val = bytes(val, default_encoding) fp.write(val) @@ -363,18 +362,18 @@ def write_DA(fp, data_element): val = data_element.value - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): write_string(fp, data_element) else: if _is_multi_value(val): - val = "\\".join((x if isinstance(x, (str, compat.string_types)) + val = "\\".join((x if isinstance(x, str) else _format_DA(x) for x in val)) else: val = _format_DA(val) if len(val) % 2 != 0: val = val + ' ' # pad to even length - if isinstance(val, compat.string_types): + if isinstance(val, str): val = val.encode(default_encoding) fp.write(val) @@ -391,18 +390,18 @@ def write_DT(fp, data_element): val = data_element.value - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): write_string(fp, data_element) else: if _is_multi_value(val): - val = "\\".join((x if isinstance(x, (str, compat.string_types)) + val = "\\".join((x if isinstance(x, str) else _format_DT(x) for x in val)) else: val = _format_DT(val) if len(val) % 2 != 0: val = val + ' ' # pad to even length - if isinstance(val, compat.string_types): + if isinstance(val, str): val = val.encode(default_encoding) fp.write(val) @@ -421,18 +420,18 @@ def write_TM(fp, data_element): val = data_element.value - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): write_string(fp, data_element) else: if _is_multi_value(val): - val = "\\".join((x if isinstance(x, (str, compat.string_types)) + val = "\\".join((x if isinstance(x, str) else _format_TM(x) for x in val)) else: val = _format_TM(val) if len(val) % 2 != 0: val = val + ' ' # pad to even length - if isinstance(val, compat.string_types): + if isinstance(val, str): val = val.encode(default_encoding) fp.write(val) @@ -512,10 +511,8 @@ # write the VR for explicit transfer syntax if not fp.is_implicit_VR: - if not in_py2: - fp.write(bytes(VR, default_encoding)) - else: - fp.write(VR) + fp.write(bytes(VR, default_encoding)) + if VR in extra_length_VRs: fp.write_US(0) # reserved 2 bytes @@ -739,6 +736,42 @@ fp.write(buffer.getvalue()) +def _write_dataset(fp, dataset, write_like_original): + """Write the Data Set to a file-like. Assumes the file meta information, + if any, has been written. + """ + + # if we want to write with the same endianess and VR handling as + # the read dataset we want to preserve raw data elements for + # performance reasons (which is done by get_item); + # otherwise we use the default converting item getter + if dataset.is_original_encoding: + get_item = Dataset.get_item + else: + get_item = Dataset.__getitem__ + + # WRITE DATASET + # The transfer syntax used to encode the dataset can't be changed + # within the dataset. + # Write any Command Set elements now as elements must be in tag order + # Mixing Command Set with other elements is non-conformant so we + # require `write_like_original` to be True + command_set = get_item(dataset, slice(0x00000000, 0x00010000)) + if command_set and write_like_original: + fp.is_implicit_VR = True + fp.is_little_endian = True + write_dataset(fp, command_set) + + # Set file VR and endianness. MUST BE AFTER writing META INFO (which + # requires Explicit VR Little Endian) and COMMAND SET (which requires + # Implicit VR Little Endian) + fp.is_implicit_VR = dataset.is_implicit_VR + fp.is_little_endian = dataset.is_little_endian + + # Write non-Command Set elements now + write_dataset(fp, get_item(dataset, slice(0x00010000, None))) + + def dcmwrite(filename, dataset, write_like_original=True): """Write `dataset` to the `filename` specified. @@ -748,9 +781,16 @@ the DICOM File Format). If `write_like_original` is ``False``, `dataset` will be stored in the - :dcm:`DICOM File Format `. The - byte stream of the `dataset` will be placed into the file after the - DICOM *File Meta Information*. + :dcm:`DICOM File Format `. To do + so requires that the ``Dataset.file_meta`` attribute + exists and contains a :class:`Dataset` with the required (Type 1) *File + Meta Information Group* elements. The byte stream of the `dataset` will be + placed into the file after the DICOM *File Meta Information*. + + If `write_like_original` is ``True`` then the :class:`Dataset` will be + written as is (after minimal validation checking) and may or may not + contain all or parts of the *File Meta Information* (and hence may or + may not be conformant with the DICOM File Format). **File Meta Information** @@ -823,7 +863,7 @@ Parameters ---------- - filename : str or file-like + filename : str or PathLike or file-like Name of file or the file-like to write the new DICOM file to. dataset : pydicom.dataset.FileDataset Dataset holding the DICOM information; e.g. an object read with @@ -848,14 +888,53 @@ If ``False``, produces a file conformant with the DICOM File Format, with explicit lengths for all elements. + Raises + ------ + AttributeError + If either ``dataset.is_implicit_VR`` or ``dataset.is_little_endian`` + have not been set. + ValueError + If group 2 elements are in ``dataset`` rather than + ``dataset.file_meta``, or if a preamble is given but is not 128 bytes + long, or if Transfer Syntax is a compressed type and pixel data is not + compressed. + See Also -------- - pydicom.dataset.FileDataset + pydicom.dataset.Dataset Dataset class with relevant attributes and information. pydicom.dataset.Dataset.save_as Write a DICOM file from a dataset that was read in with ``dcmread()``. ``save_as()`` wraps ``dcmwrite()``. """ + + # Ensure is_little_endian and is_implicit_VR are set + if None in (dataset.is_little_endian, dataset.is_implicit_VR): + has_tsyntax = False + try: + tsyntax = dataset.file_meta.TransferSyntaxUID + if not tsyntax.is_private: + dataset.is_little_endian = tsyntax.is_little_endian + dataset.is_implicit_VR = tsyntax.is_implicit_VR + has_tsyntax = True + except AttributeError: + pass + + if not has_tsyntax: + raise AttributeError( + "'{0}.is_little_endian' and '{0}.is_implicit_VR' must be " + "set appropriately before saving." + .format(dataset.__class__.__name__) + ) + + # Try and ensure that `is_undefined_length` is set correctly + try: + tsyntax = dataset.file_meta.TransferSyntaxUID + if not tsyntax.is_private: + dataset['PixelData'].is_undefined_length = tsyntax.is_compressed + except (AttributeError, KeyError): + pass + # Check that dataset's group 0x0002 elements are only present in the # `dataset.file_meta` Dataset - user may have added them to the wrong # place @@ -896,22 +975,14 @@ caller_owns_file = True # Open file if not already a file object - if isinstance(filename, compat.string_types): + filename = path_from_pathlike(filename) + if isinstance(filename, str): fp = DicomFile(filename, 'wb') # caller provided a file name; we own the file handle caller_owns_file = False else: fp = DicomFileLike(filename) - # if we want to write with the same endianess and VR handling as - # the read dataset we want to preserve raw data elements for - # performance reasons (which is done by get_item); - # otherwise we use the default converting item getter - if dataset.is_original_encoding: - get_item = Dataset.get_item - else: - get_item = Dataset.__getitem__ - try: # WRITE FILE META INFORMATION if preamble: @@ -919,31 +990,32 @@ fp.write(preamble) fp.write(b'DICM') + tsyntax = None if dataset.file_meta: # May be an empty Dataset # If we want to `write_like_original`, don't enforce_standard write_file_meta_info(fp, dataset.file_meta, enforce_standard=not write_like_original) + tsyntax = getattr(dataset.file_meta, "TransferSyntaxUID", None) - # WRITE DATASET - # The transfer syntax used to encode the dataset can't be changed - # within the dataset. - # Write any Command Set elements now as elements must be in tag order - # Mixing Command Set with other elements is non-conformant so we - # require `write_like_original` to be True - command_set = get_item(dataset, slice(0x00000000, 0x00010000)) - if command_set and write_like_original: - fp.is_implicit_VR = True - fp.is_little_endian = True - write_dataset(fp, command_set) - - # Set file VR and endianness. MUST BE AFTER writing META INFO (which - # requires Explicit VR Little Endian) and COMMAND SET (which requires - # Implicit VR Little Endian) - fp.is_implicit_VR = dataset.is_implicit_VR - fp.is_little_endian = dataset.is_little_endian + if (tsyntax == DeflatedExplicitVRLittleEndian): + # See PS3.5 section A.5 + # when writing, the entire dataset following + # the file metadata is prepared the normal way, + # then "deflate" compression applied. + buffer = DicomBytesIO() + _write_dataset(buffer, dataset, write_like_original) + + # Compress the encoded data and write to file + compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS) + deflated = compressor.compress(buffer.parent.getvalue()) + deflated += compressor.flush() + if len(deflated) % 2: + deflated += b'\x00' + + fp.write(deflated) + else: + _write_dataset(fp, dataset, write_like_original) - # Write non-Command Set elements now - write_dataset(fp, get_item(dataset, slice(0x00010000, None))) finally: if not caller_owns_file: fp.close() @@ -969,7 +1041,7 @@ 'LT': (write_text, None), 'OB': (write_OBvalue, None), 'OD': (write_OWvalue, None), - 'OF': (write_numbers, 'f'), + 'OF': (write_OWvalue, None), 'OL': (write_OWvalue, None), 'OW': (write_OWvalue, None), 'OV': (write_OWvalue, None), diff -Nru pydicom-1.4.1/pydicom/__init__.py pydicom-2.0.0/pydicom/__init__.py --- pydicom-1.4.1/pydicom/__init__.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/__init__.py 2020-05-29 01:44:31.000000000 +0000 @@ -48,8 +48,3 @@ '__version__', '__version_info__'] -from pydicom.compat import in_py2 -if in_py2: - import warnings - msg = 'Python 2 will no longer be supported after the pydicom v1.4 release' - warnings.warn(msg, DeprecationWarning) diff -Nru pydicom-1.4.1/pydicom/jsonrep.py pydicom-2.0.0/pydicom/jsonrep.py --- pydicom-1.4.1/pydicom/jsonrep.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/jsonrep.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,9 +4,7 @@ import base64 import warnings -from pydicom import compat -from pydicom.compat import int_type -from pydicom.valuerep import PersonNameUnicode +from pydicom.valuerep import PersonName # Order of keys is significant! JSON_VALUE_KEYS = ('Value', 'BulkDataURI', 'InlineBinary',) @@ -39,7 +37,7 @@ return None number_type = None if vr in VRs_TO_BE_INTS: - number_type = int_type + number_type = int if vr in VRs_TO_BE_FLOATS: number_type = float if number_type is not None: @@ -50,7 +48,7 @@ return value -class JsonDataElementConverter(object): +class JsonDataElementConverter: """Handles conversion between JSON struct and :class:`DataElement`. .. versionadded:: 1.4 @@ -91,7 +89,7 @@ Returns ------- str or bytes or int or float or dataset_class - or PersonName3 or PersonNameUnicode or list of any of these types + or PersonName or list of any of these types The value or value list of the newly created data element. """ from pydicom.dataelem import empty_value_for_VR @@ -117,13 +115,13 @@ value = value[0] if self.value_key == 'InlineBinary': - if not isinstance(value, compat.char_types): + if not isinstance(value, (str, bytes)): fmt = '"{}" of data element "{}" must be a bytes-like object.' raise TypeError(fmt.format(self.value_key, self.tag)) return base64.b64decode(value) if self.value_key == 'BulkDataURI': - if not isinstance(value, compat.string_types): + if not isinstance(value, str): fmt = '"{}" of data element "{}" must be a string.' raise TypeError(fmt.format(self.value_key, self.tag)) if self.bulk_data_uri_handler is None: @@ -145,7 +143,7 @@ Returns ------- - dataset_class or PersonName3 or PersonNameUnicode + dataset_class or PersonName or str or int or float A single value of the corresponding :class:`DataElement`. """ @@ -222,7 +220,7 @@ Returns ------- - PersonName3 or PersonNameUnicode or str + PersonName or str The decoded PersonName object or an empty string. """ if not isinstance(value, dict): @@ -248,6 +246,4 @@ if 'Phonetic' in value: comps[2] = value['Phonetic'] elem_value = '='.join(comps) - if compat.in_py2: - elem_value = PersonNameUnicode(elem_value, 'UTF8') return elem_value diff -Nru pydicom-1.4.1/pydicom/multival.py pydicom-2.0.0/pydicom/multival.py --- pydicom-1.4.1/pydicom/multival.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/multival.py 2020-05-29 01:44:31.000000000 +0000 @@ -2,7 +2,6 @@ """Code for multi-value data elements values, or any list of items that must all be the same type. """ -from pydicom import compat try: from collections.abc import MutableSequence @@ -49,13 +48,6 @@ for x in iterable: self._list.append(type_constructor(x)) - # TODO: Workaround for #951, to be removed when Python 2 not supported - if compat.in_py2: - def __getstate__(self): - state = self.__dict__.copy() - del state['type_constructor'] - return state - def insert(self, position, val): self._list.insert(position, self.type_constructor(val)) @@ -73,7 +65,7 @@ def __str__(self): if not self: return '' - lines = ["'{}'".format(x) if isinstance(x, compat.char_types) + lines = ["'{}'".format(x) if isinstance(x, (str, bytes)) else str(x) for x in self] return "[" + ", ".join(lines) + "]" diff -Nru pydicom-1.4.1/pydicom/pixel_data_handlers/gdcm_handler.py pydicom-2.0.0/pydicom/pixel_data_handlers/gdcm_handler.py --- pydicom-1.4.1/pydicom/pixel_data_handlers/gdcm_handler.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/pixel_data_handlers/gdcm_handler.py 2020-05-29 01:44:31.000000000 +0000 @@ -21,7 +21,6 @@ HAVE_GDCM_IN_MEMORY_SUPPORT = False import pydicom.uid -from pydicom import compat from pydicom.pixel_data_handlers.util import get_expected_length, pixel_dtype @@ -175,14 +174,7 @@ gdcm.ImageReader """ image_reader = gdcm.ImageReader() - if compat.in_py2: - if isinstance(filename, unicode): - image_reader.SetFileName( - filename.encode(sys.getfilesystemencoding())) - else: - image_reader.SetFileName(filename) - else: - image_reader.SetFileName(filename) + image_reader.SetFileName(filename) return image_reader @@ -219,19 +211,16 @@ raise TypeError("GDCM could not read DICOM image") gdcm_image = gdcm_image_reader.GetImage() - # GDCM returns char* as type str. Under Python 2 `str` are - # byte arrays by default. Python 3 decodes this to + # GDCM returns char* as type str. Python 3 decodes this to # unicode strings by default. # The SWIG docs mention that they always decode byte streams # as utf-8 strings for Python 3, with the `surrogateescape` # error handler configured. # Therefore, we can encode them back to their original bytearray # representation on Python 3 by using the same parameters. - if compat.in_py2: - pixel_bytearray = gdcm_image.GetBuffer() - else: - pixel_bytearray = gdcm_image.GetBuffer().encode( - "utf-8", "surrogateescape") + + pixel_bytearray = gdcm_image.GetBuffer().encode( + "utf-8", "surrogateescape") # Here we need to be careful because in some cases, GDCM reads a # buffer that is too large, so we need to make sure we only include diff -Nru pydicom-1.4.1/pydicom/pixel_data_handlers/pillow_handler.py pydicom-2.0.0/pydicom/pixel_data_handlers/pillow_handler.py --- pydicom-1.4.1/pydicom/pixel_data_handlers/pillow_handler.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/pixel_data_handlers/pillow_handler.py 2020-05-29 01:44:31.000000000 +0000 @@ -31,13 +31,7 @@ logger = logging.getLogger('pydicom') -PillowSupportedTransferSyntaxes = [ - pydicom.uid.JPEGBaseline, - pydicom.uid.JPEGLossless, - pydicom.uid.JPEGExtended, - pydicom.uid.JPEG2000, - pydicom.uid.JPEG2000Lossless, -] + PillowJPEG2000TransferSyntaxes = [ pydicom.uid.JPEG2000, pydicom.uid.JPEG2000Lossless, @@ -46,6 +40,10 @@ pydicom.uid.JPEGBaseline, pydicom.uid.JPEGExtended, ] +PillowSupportedTransferSyntaxes = ( + PillowJPEGTransferSyntaxes + PillowJPEG2000TransferSyntaxes +) + HANDLER_NAME = 'Pillow' @@ -115,6 +113,8 @@ logger.debug("Trying to use Pillow to read pixel array " "(has pillow = %s)", HAVE_PIL) transfer_syntax = ds.file_meta.TransferSyntaxUID + logger.debug("Transfer Syntax UID: '{}'".format(transfer_syntax)) + if not HAVE_PIL: msg = ("The pillow package is required to use pixel_array for " "this transfer syntax {0}, and pillow could not be " @@ -133,21 +133,11 @@ .format(transfer_syntax.name)) raise NotImplementedError(msg) - if transfer_syntax not in PillowSupportedTransferSyntaxes: - msg = ("this transfer syntax {0}, can not be read because " - "Pillow does not support this syntax" - .format(transfer_syntax.name)) - raise NotImplementedError(msg) - - if transfer_syntax in PillowJPEGTransferSyntaxes: - logger.debug("This is a JPEG lossy format") - if ds.BitsAllocated > 8: - raise NotImplementedError("JPEG Lossy only supported if " - "Bits Allocated = 8") - elif transfer_syntax in PillowJPEG2000TransferSyntaxes: - logger.debug("This is a JPEG 2000 format") - else: - logger.debug("This is a another pillow supported format") + if transfer_syntax == pydicom.uid.JPEGExtended and ds.BitsAllocated != 8: + raise NotImplementedError( + "{} - {} only supported by Pillow if Bits Allocated = 8" + .format(pydicom.uid.JPEGExtended, pydicom.uid.JPEGExtended.name) + ) pixel_bytes = bytearray() if getattr(ds, 'NumberOfFrames', 1) > 1: diff -Nru pydicom-1.4.1/pydicom/pixel_data_handlers/util.py pydicom-2.0.0/pydicom/pixel_data_handlers/util.py --- pydicom-1.4.1/pydicom/pixel_data_handlers/util.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/pixel_data_handlers/util.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,6 +1,5 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Utility functions used in the pixel data handlers.""" -from __future__ import division from struct import unpack from sys import byteorder @@ -113,6 +112,9 @@ "Table Module is not currently supported" ) + if 'RedPaletteColorLookupTableDescriptor' not in ds: + raise ValueError("No suitable Palette Color Lookup Table Module found") + # All channels are supposed to be identical lut_desc = ds.RedPaletteColorLookupTableDescriptor # A value of 0 = 2^16 entries @@ -231,7 +233,15 @@ nominal_depth = item.LUTDescriptor[2] dtype = 'uint{}'.format(nominal_depth) - lut_data = np.asarray(item.LUTData, dtype=dtype) + + # Ambiguous VR, US or OW + if item['LUTData'].VR == 'OW': + endianness = '<' if ds.is_little_endian else '>' + unpack_fmt = '{}{}H'.format(endianness, nr_entries) + lut_data = unpack(unpack_fmt, item.LUTData) + else: + lut_data = item.LUTData + lut_data = np.asarray(lut_data, dtype=dtype) # IVs < `first_map` get set to first LUT entry (i.e. index 0) clipped_iv = np.zeros(arr.shape, dtype=arr.dtype) @@ -314,7 +324,14 @@ .format(nominal_depth) ) - lut_data = np.asarray(item.LUTData, dtype=dtype) + # Ambiguous VR, US or OW + if item['LUTData'].VR == 'OW': + endianness = '<' if ds.is_little_endian else '>' + unpack_fmt = '{}{}H'.format(endianness, nr_entries) + lut_data = unpack(unpack_fmt, item.LUTData) + else: + lut_data = item.LUTData + lut_data = np.asarray(lut_data, dtype=dtype) # IVs < `first_map` get set to first LUT entry (i.e. index 0) clipped_iv = np.zeros(arr.shape, dtype=arr.dtype) diff -Nru pydicom-1.4.1/pydicom/sr/codedict.py pydicom-2.0.0/pydicom/sr/codedict.py --- pydicom-1.4.1/pydicom/sr/codedict.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/sr/codedict.py 2020-05-29 01:44:31.000000000 +0000 @@ -28,7 +28,7 @@ return sorted(allnames) -class _CID_Dict(object): +class _CID_Dict: repr_format = "{} = {}" str_format = "{:20} {:12} {:8} {}\n" @@ -169,7 +169,7 @@ return dir(self) -class _CodesDict(object): +class _CodesDict: def __init__(self, scheme=None): self.scheme = scheme if scheme: diff -Nru pydicom-1.4.1/pydicom/_storage_sopclass_uids.py pydicom-2.0.0/pydicom/_storage_sopclass_uids.py --- pydicom-1.4.1/pydicom/_storage_sopclass_uids.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/_storage_sopclass_uids.py 2020-05-29 01:44:31.000000000 +0000 @@ -2,7 +2,6 @@ Storage SOP Class UIDs auto-generated by generate_storage_sopclass_uids.py """ -from __future__ import absolute_import from pydicom.uid import UID MediaStorageDirectoryStorage = UID( diff -Nru pydicom-1.4.1/pydicom/tag.py pydicom-2.0.0/pydicom/tag.py --- pydicom-1.4.1/pydicom/tag.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tag.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,9 +1,8 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Define Tag class to hold a DICOM (group, element) tag and related functions. -The 4 bytes of the DICOM tag are stored as an arbitrary length 'long' for -Python 2 and as an 'int' for Python 3. Tags are stored as a single number and -separated to (group, element) as required. +The 4 bytes of the DICOM tag are stored as an 'int'. Tags are +stored as a single number and separated to (group, element) as required. """ # NOTE: Tags must be not be stored as a tuple internally, as some code logic # (e.g. in filewriter.write_AT) checks if a value is a multi-value @@ -11,8 +10,6 @@ import traceback from contextlib import contextmanager -from pydicom import compat - @contextmanager def tag_in_exception(tag): @@ -83,12 +80,12 @@ raise ValueError("Tag must be an int or a 2-tuple") valid = False - if isinstance(arg[0], compat.string_types): - valid = isinstance(arg[1], (str, compat.string_types)) + if isinstance(arg[0], str): + valid = isinstance(arg[1], str) if valid: arg = (int(arg[0], 16), int(arg[1], 16)) - elif isinstance(arg[0], compat.number_types): - valid = isinstance(arg[1], compat.number_types) + elif isinstance(arg[0], int): + valid = isinstance(arg[1], int) if not valid: raise ValueError("Both arguments for Tag must be the same type, " "either string or int.") @@ -100,7 +97,7 @@ long_value = (arg[0] << 16) | arg[1] # Single str parameter - elif isinstance(arg, (str, compat.text_type)): + elif isinstance(arg, str): try: long_value = int(arg, 16) if long_value > 0xFFFFFFFF: @@ -127,18 +124,10 @@ return BaseTag(long_value) -if compat.in_py2: - # May get an overflow error with int if sys.maxsize < 0xFFFFFFFF - BaseTag_base_class = long -else: - BaseTag_base_class = int - - -class BaseTag(BaseTag_base_class): +class BaseTag(int): """Represents a DICOM element (group, element) tag. - If using Python 2.7 then tags are represented as a :class:`long`, while for - Python 3 they are represented as an :class:`int`. + Tags are represented as an :class:`int`. Attributes ---------- @@ -166,7 +155,7 @@ except Exception: raise TypeError("Cannot compare Tag with non-Tag item") - return BaseTag_base_class(self) < BaseTag_base_class(other) + return int(self) < int(other) def __ge__(self, other): """Return ``True`` if `self` is greater than or equal to `other`.""" @@ -179,13 +168,13 @@ def __eq__(self, other): """Return ``True`` if `self` equals `other`.""" # Check if comparing with another Tag object; if not, create a temp one - if not isinstance(other, BaseTag_base_class): + if not isinstance(other, int): try: other = Tag(other) except Exception: raise TypeError("Cannot compare Tag with non-Tag item") - return BaseTag_base_class(self) == BaseTag_base_class(other) + return int(self) == int(other) def __ne__(self, other): """Return ``True`` if `self` does not equal `other`.""" @@ -196,7 +185,7 @@ # to the parent class # See http://docs.python.org/dev/3.0/reference/ # datamodel.html#object.__hash__ - __hash__ = BaseTag_base_class.__hash__ + __hash__ = int.__hash__ def __str__(self): """Return the tag value as a hex string '(gggg, eeee)'.""" diff -Nru pydicom-1.4.1/pydicom/tests/test_charset.py pydicom-2.0.0/pydicom/tests/test_charset.py --- pydicom-1.4.1/pydicom/tests/test_charset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_charset.py 2020-05-29 01:44:31.000000000 +0000 @@ -9,7 +9,7 @@ from pydicom.data import get_charset_files, get_testdata_files from pydicom.dataelem import DataElement from pydicom.filebase import DicomBytesIO -from pydicom.valuerep import PersonName3 +from pydicom.valuerep import PersonName # The file names (without '.dcm' extension) of most of the character test # files, together with the respective decoded PatientName tag values. @@ -61,7 +61,7 @@ ] -class TestCharset(object): +class TestCharset: def teardown(self): config.enforce_valid_values = False @@ -133,10 +133,9 @@ def test_bad_charset(self): """Test bad charset defaults to ISO IR 6""" - # Python 3: elem.value is PersonName3, Python 2: elem.value is str + # elem.value is PersonName elem = DataElement(0x00100010, 'PN', 'CITIZEN') pydicom.charset.decode_element(elem, ['ISO 2022 IR 126']) - # After decode Python 2: elem.value is PersonNameUnicode assert 'iso_ir_126' in elem.value.encodings assert 'iso8859' not in elem.value.encodings # default encoding is iso8859 @@ -385,7 +384,7 @@ assert patient_name == ds.PatientName # check that patient names are correctly written back - # without original byte string (PersonName3 only) + # without original byte string (PersonName only) if hasattr(ds.PatientName, 'original_string'): ds.PatientName.original_string = None fp = DicomBytesIO() @@ -436,7 +435,7 @@ ds_out = dcmread(fp) assert original_string == ds_out.PatientName.original_string - japanese_pn = PersonName3(u"Mori^Ogai=森^鷗外=もり^おうがい") + japanese_pn = PersonName(u"Mori^Ogai=森^鷗外=もり^おうがい") pyencs = pydicom.charset.convert_encodings(["ISO 2022 IR 6", "ISO 2022 IR 87", "ISO 2022 IR 159"]) @@ -468,7 +467,7 @@ def test_deprecated_decode(self): """Test we get a deprecation warning when using charset.decode().""" - # Python 3: elem.value is PersonName3, Python 2: elem.value is str + # elem.value is PersonName elem = DataElement(0x00100010, 'PN', 'CITIZEN') msg = r"'charset.decode\(\)' is deprecated" with pytest.warns(DeprecationWarning, match=msg): diff -Nru pydicom-1.4.1/pydicom/tests/test_codedict.py pydicom-2.0.0/pydicom/tests/test_codedict.py --- pydicom-1.4.1/pydicom/tests/test_codedict.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_codedict.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,15 +1,10 @@ -import unittest - import pytest from pydicom.sr.codedict import codes from pydicom.sr.coding import Code -class TestCodeDict(unittest.TestCase): - def setUp(self): - super(TestCodeDict, self).setUp() - +class TestCodeDict: def test_dcm_1(self): assert codes.DCM.Modality == Code( value="121139", scheme_designator="DCM", meaning="Modality" diff -Nru pydicom-1.4.1/pydicom/tests/test_coding.py pydicom-2.0.0/pydicom/tests/test_coding.py --- pydicom-1.4.1/pydicom/tests/test_coding.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_coding.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,14 +1,11 @@ -import unittest - import pytest from pydicom.sr.coding import Code from pydicom.uid import UID -class TestCode(unittest.TestCase): - def setUp(self): - super(TestCode, self).setUp() +class TestCode: + def setup(self): self._value = "373098007" self._meaning = "Mean Value of population" self._scheme_designator = "SCT" diff -Nru pydicom-1.4.1/pydicom/tests/test_compat.py pydicom-2.0.0/pydicom/tests/test_compat.py --- pydicom-1.4.1/pydicom/tests/test_compat.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_compat.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -# Copyright 2008-2018 pydicom authors. See LICENSE file for details. -"""Tests for dataset.py""" - -import sys - -import pytest - -from pydicom.compat import * - - -def test_python_version(): - """Test that the correct python version is returned""" - if sys.version_info[0] == 2: - assert in_py2 - assert text_type == unicode - assert string_types == (str, unicode) - else: - assert not in_py2 - assert text_type == str - assert string_types == (str,) - - # Kinda redundant - assert in_PyPy == ('PyPy' in sys.version) - - -def test_reraise(): - """Test reraising an exception works in both py2 and 3""" - def raiser(): - raise ValueError('Some msg') - - with pytest.raises(ValueError) as exc: - reraise(raiser()) - - assert str(exc.value) == 'Some msg' diff -Nru pydicom-1.4.1/pydicom/tests/test_config.py pydicom-2.0.0/pydicom/tests/test_config.py --- pydicom-1.4.1/pydicom/tests/test_config.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_config.py 2020-05-29 01:44:31.000000000 +0000 @@ -16,7 +16,7 @@ @pytest.mark.skipif(PYTEST[:2] < [3, 4], reason='no caplog') -class TestDebug(object): +class TestDebug: """Tests for config.debug().""" def setup(self): self.logger = logging.getLogger('pydicom') diff -Nru pydicom-1.4.1/pydicom/tests/test_dataelem.py pydicom-2.0.0/pydicom/tests/test_dataelem.py --- pydicom-1.4.1/pydicom/tests/test_dataelem.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_dataelem.py 2020-05-29 01:44:31.000000000 +0000 @@ -24,7 +24,7 @@ from pydicom.valuerep import DSfloat -class TestDataElement(object): +class TestDataElement: """Tests for dataelem.DataElement.""" def setup(self): self.data_elementSH = DataElement((1, 2), "SH", "hello") @@ -104,7 +104,7 @@ assert 'Private tag data' == elem.description() elem = DataElement(0x00110F00, 'LO', 12345) assert elem.tag.is_private - assert not hasattr(elem, 'private_creator') + assert elem.private_creator is None assert 'Private tag data' == elem.description() def test_description_unknown(self): @@ -281,28 +281,6 @@ elem[0].PatientID = '1234' assert repr(elem) == repr(elem.value) - @pytest.mark.skipif(sys.version_info >= (3,), reason='Python 2 behavior') - def test_unicode(self): - """Test unicode representation of the DataElement""" - elem = DataElement(0x00100010, 'PN', u'ANON') - # Make sure elem.value is actually unicode - assert isinstance(elem.value, unicode) - assert ( - u"(0010, 0010) Patient's Name PN: ANON" - ) == unicode(elem) - assert isinstance(unicode(elem), unicode) - assert not isinstance(unicode(elem), str) - # Make sure elem.value is still unicode - assert isinstance(elem.value, unicode) - - # When value is not in compat.text_type - elem = DataElement(0x00100010, 'LO', 12345) - assert isinstance(unicode(elem), unicode) - assert ( - u"(0010, 0010) Patient's Name" - u" LO: 12345" - ) == unicode(elem) - def test_getitem_raises(self): """Test DataElement.__getitem__ raise if value not indexable""" elem = DataElement(0x00100010, 'LO', 12345) @@ -533,7 +511,7 @@ assert elem.value == [] -class TestRawDataElement(object): +class TestRawDataElement: """Tests for dataelem.RawDataElement.""" def test_key_error(self): """RawDataElement: conversion of unknown tag throws KeyError...""" diff -Nru pydicom-1.4.1/pydicom/tests/test_data_manager.py pydicom-2.0.0/pydicom/tests/test_data_manager.py --- pydicom-1.4.1/pydicom/tests/test_data_manager.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_data_manager.py 2020-05-29 01:44:31.000000000 +0000 @@ -12,7 +12,7 @@ from pydicom.data.data_manager import DATA_ROOT, get_testdata_file -class TestGetData(object): +class TestGetData: def test_get_dataset(self): """Test the different functions to get lists of data files.""" # Test base locations diff -Nru pydicom-1.4.1/pydicom/tests/test_dataset.py pydicom-2.0.0/pydicom/tests/test_dataset.py --- pydicom-1.4.1/pydicom/tests/test_dataset.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_dataset.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,10 +4,11 @@ import pytest import pydicom -from pydicom import compat -from pydicom.data import get_testdata_files +from pydicom.data import get_testdata_file from pydicom.dataelem import DataElement, RawDataElement -from pydicom.dataset import Dataset, FileDataset, validate_file_meta +from pydicom.dataset import ( + Dataset, FileDataset, validate_file_meta, FileMetaDataset +) from pydicom.encaps import encapsulate from pydicom import dcmread from pydicom.filebase import DicomBytesIO @@ -23,12 +24,12 @@ ) -class BadRepr(object): +class BadRepr: def __repr__(self): raise ValueError("bad repr") -class TestDataset(object): +class TestDataset: """Tests for dataset.Dataset.""" def setup(self): self.ds = Dataset() @@ -39,10 +40,10 @@ # This comes from bug fix for issue 42 # First, fake enough to try the pixel_array property ds = Dataset() - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.PixelData = 'xyzlmnop' msg_from_gdcm = r"'Dataset' object has no attribute 'filename'" - msg_from_numpy = (r"'Dataset' object has no attribute " + msg_from_numpy = (r"'FileMetaDataset' object has no attribute " "'TransferSyntaxUID'") msg_from_pillow = (r"'Dataset' object has no attribute " "'PixelRepresentation'") @@ -58,6 +59,8 @@ sub_ds.BeamNumber = '1' dataset.BeamSequence = Sequence([sub_ds]) fp = DicomBytesIO() + dataset.is_little_endian = True + dataset.is_implicit_VR = True pydicom.write_file(fp, dataset) def _reset(): @@ -751,39 +754,6 @@ assert 0x00090010 not in ds assert 'PatientName' in ds - @pytest.mark.skipif(not compat.in_py2, reason='Python 2 only iterators') - def test_iteritems(self): - ds = Dataset() - ds.Overlays = 12 # 0000,51B0 - ds.LengthToEnd = 12 # 0008,0001 - ds.SOPInstanceUID = '1.2.3.4' # 0008,0018 - ds.SkipFrameRangeFlag = 'TEST' # 0008,9460 - - keys = [] - for key in ds.iterkeys(): - keys.append(key) - assert 4 == len(keys) - assert 0x000051B0 in keys - assert 0x00089460 in keys - - values = [] - for value in ds.itervalues(): - values.append(value) - - assert 4 == len(values) - assert DataElement(0x00080018, 'UI', '1.2.3.4') in values - assert DataElement(0x00089460, 'CS', 'TEST') in values - - items = {} - for key, value in ds.iteritems(): - items[key] = value - - assert 4 == len(items) - assert 0x000051B0 in items - assert 0x00080018 in items - assert '1.2.3.4' == items[0x00080018].value - assert 12 == items[0x00080001].value - def test_group_dataset(self): """Test Dataset.group_dataset""" ds = Dataset() @@ -825,7 +795,7 @@ assert '1.2.3.4' == ds.get_item(0x00080018).value # Test deferred read - test_file = get_testdata_files('MR_small.dcm')[0] + test_file = get_testdata_file('MR_small.dcm') ds = dcmread(test_file, force=True, defer_size='0.8 kB') ds_ref = dcmread(test_file, force=True) # get_item will follow the deferred read branch @@ -949,9 +919,10 @@ ds.add_new(0x00080005, 'CS', 'ISO_IR 100') ds.add_new(0x00090010, 'LO', 'Creator 1.0') ds.add_new(0x00091001, 'SH', 'Version1') - ds.add_new(0x00090011, 'LO', 'Creator 2.0') - ds.add_new(0x00091101, 'SH', 'Version2') - ds.add_new(0x00091102, 'US', 2) + # make sure it works with non-contiguous blocks + ds.add_new(0x00090020, 'LO', 'Creator 2.0') + ds.add_new(0x00092001, 'SH', 'Version2') + ds.add_new(0x00092002, 'US', 2) # Dataset.private_block with pytest.raises(ValueError, match='Tag must be private'): @@ -987,6 +958,26 @@ item = ds.get_private_item(0x0009, 0x02, 'Creator 2.0') assert 2 == item.value + def test_private_creator_from_raw_ds(self): + # regression test for #1078 + ct_filename = get_testdata_file("CT_small.dcm") + ds = dcmread(ct_filename) + ds.private_block(0x11, 'GEMS_PATI_01', create=True) + assert ['GEMS_PATI_01'] == ds.private_creators(0x11) + + assert [] == ds.private_creators(0x13) + ds.private_block(0x13, 'GEMS_PATI_01', create=True) + assert ['GEMS_PATI_01'] == ds.private_creators(0x13) + + def test_add_known_private_tag(self): + # regression test for #1082 + ds = dcmread(get_testdata_file("CT_small.dcm")) + assert '[Patient Status]' == ds[0x11, 0x1010].name + + block = ds.private_block(0x11, 'GEMS_PATI_01') + block.add_new(0x10, 'SS', 1) + assert '[Patient Status]' == ds[0x11, 0x1010].name + def test_add_new_private_tag(self): ds = Dataset() ds.add_new(0x00080005, 'CS', 'ISO_IR 100') @@ -1031,6 +1022,68 @@ assert ['Creator 1.0', 'Creator 2.0'] == ds.private_creators(0x0009) assert not ds.private_creators(0x0011) + def test_non_contiguous_private_creators(self): + ds = Dataset() + ds.add_new(0x00080005, 'CS', 'ISO_IR 100') + ds.add_new(0x00090010, 'LO', 'Creator 1.0') + ds.add_new(0x00090020, 'LO', 'Creator 2.0') + ds.add_new(0x000900ff, 'LO', 'Creator 3.0') + + assert (['Creator 1.0', 'Creator 2.0', 'Creator 3.0'] == + ds.private_creators(0x0009)) + + def test_create_private_tag_after_removing_all(self): + # regression test for #1097 - make sure private blocks are updated + # after removing all private tags + ds = Dataset() + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x01, "SH", "Border Collie") + block = ds.private_block(0x000b, 'dog^2', create=True) + block.add_new(0x01, "SH", "Poodle") + + ds.remove_private_tags() + block = ds.private_block(0x000b, 'dog^2', create=True) + block.add_new(0x01, "SH", "Poodle") + assert len(ds) == 2 + assert (0x000b0010) in ds + assert ds[0x000b0010].value == 'dog^2' + + def test_create_private_tag_after_removing_private_creator(self): + ds = Dataset() + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x01, "SH", "Border Collie") + + del ds[0x000b0010] + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x02, "SH", "Poodle") + assert len(ds) == 3 + assert ds[0x000b0010].value == 'dog^1' + + del ds[Tag(0x000b, 0x0010)] + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x01, "SH", "Pug") + assert len(ds) == 3 + assert ds[0x000b0010].value == 'dog^1' + + del ds['0x000b0010'] + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x01, "SH", "Pug") + assert len(ds) == 3 + assert ds[0x000b0010].value == 'dog^1' + + def test_create_private_tag_after_removing_slice(self): + ds = Dataset() + block = ds.private_block(0x000b, 'dog^1', create=True) + block.add_new(0x01, "SH", "Border Collie") + block = ds.private_block(0x000b, 'dog^2', create=True) + block.add_new(0x01, "SH", "Poodle") + + del ds[0x000b0010:0x000b1110] + block = ds.private_block(0x000b, 'dog^2', create=True) + block.add_new(0x01, "SH", "Poodle") + assert len(ds) == 2 + assert ds[0x000b0010].value == 'dog^2' + def test_is_original_encoding(self): """Test Dataset.write_like_original""" ds = Dataset() @@ -1117,7 +1170,7 @@ ds.save_as(fp, write_like_original=False) ds.is_implicit_VR = True - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.MediaStorageSOPClassUID = '1.1' ds.file_meta.MediaStorageSOPInstanceUID = '1.2' ds.file_meta.TransferSyntaxUID = '1.3' @@ -1130,7 +1183,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = JPEGBaseline ds.PixelData = b'\x00\x01\x02\x03\x04\x05\x06' ds['PixelData'].VR = 'OB' @@ -1147,7 +1200,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = JPEGBaseline ds.PixelData = encapsulate([b'\x00\x01\x02\x03\x04\x05\x06']) ds['PixelData'].VR = 'OB' @@ -1159,7 +1212,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = JPEGBaseline ds.save_as(fp) @@ -1169,7 +1222,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.save_as(fp) del ds.file_meta @@ -1181,7 +1234,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = '1.2.3.4.5.6' ds.save_as(fp) @@ -1199,7 +1252,7 @@ ds.save_as(fp) # Test private transfer syntax raises - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = '1.2' with pytest.raises(AttributeError, match=msg): ds.save_as(fp) @@ -1214,7 +1267,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = JPEGBaseline ds.PixelData = encapsulate([b'\x00\x01\x02\x03\x04\x05\x06']) elem = ds['PixelData'] @@ -1243,7 +1296,7 @@ ds = Dataset() ds.is_little_endian = True ds.is_implicit_VR = False - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = '1.2.3.4.5' ds.PixelData = encapsulate([b'\x00\x01\x02\x03\x04\x05\x06']) elem = ds['PixelData'] @@ -1277,7 +1330,7 @@ def test_with(self): """Test Dataset.__enter__ and __exit__.""" - test_file = get_testdata_files('CT_small.dcm')[0] + test_file = get_testdata_file('CT_small.dcm') with dcmread(test_file) as ds: assert 'CompressedSamples^CT1' == ds.PatientName @@ -1294,7 +1347,7 @@ def test_pixel_array_already_have(self): """Test Dataset._get_pixel_array when we already have the array""" # Test that _pixel_array is returned unchanged unless required - fpath = get_testdata_files("CT_small.dcm")[0] + fpath = get_testdata_file("CT_small.dcm") ds = dcmread(fpath) ds._pixel_id = get_image_pixel_ids(ds) ds._pixel_array = 'Test Value' @@ -1304,7 +1357,7 @@ def test_pixel_array_id_changed(self): """Test that we try to get new pixel data if the id has changed.""" - fpath = get_testdata_files("CT_small.dcm")[0] + fpath = get_testdata_file("CT_small.dcm") ds = dcmread(fpath) ds.file_meta.TransferSyntaxUID = '1.2.3.4' ds._pixel_id = 1234 @@ -1320,7 +1373,7 @@ def test_pixel_array_unknown_syntax(self): """Test that pixel_array for an unknown syntax raises exception.""" - ds = dcmread(get_testdata_files("CT_small.dcm")[0]) + ds = dcmread(get_testdata_file("CT_small.dcm")) ds.file_meta.TransferSyntaxUID = '1.2.3.4' msg = ( r"Unable to decode pixel data with a transfer syntax UID of " @@ -1359,7 +1412,7 @@ def test_set_convert_private_elem_from_raw(self): """Test Dataset.__setitem__ with a raw private element""" - test_file = get_testdata_files('CT_small.dcm')[0] + test_file = get_testdata_file('CT_small.dcm') ds = dcmread(test_file, force=True) # 'tag VR length value value_tell is_implicit_VR is_little_endian' elem = RawDataElement((0x0043, 0x1029), 'OB', 2, b'\x00\x01', 0, @@ -1380,7 +1433,7 @@ def test_trait_names(self): """Test Dataset.trait_names contains element keywords""" - test_file = get_testdata_files('CT_small.dcm')[0] + test_file = get_testdata_file('CT_small.dcm') ds = dcmread(test_file, force=True) names = ds.trait_names() assert 'PatientName' in names @@ -1390,7 +1443,7 @@ def test_walk(self): """Test Dataset.walk iterates through sequences""" def test_callback(dataset, elem): - if elem.keyword is 'PatientID': + if elem.keyword == 'PatientID': dataset.PatientID = 'FIXED' ds = Dataset() @@ -1432,7 +1485,7 @@ assert 'TestC' == ds2.PatientName -class TestDatasetElements(object): +class TestDatasetElements: """Test valid assignments of data elements""" def setup(self): self.ds = Dataset() @@ -1488,6 +1541,8 @@ assert '4.5.6' == self.ds.file_meta.MediaStorageSOPInstanceUID self.ds.fix_meta_info(enforce_standard=True) + with pytest.warns(DeprecationWarning): + self.ds.file_meta = Dataset() # not FileMetaDataset self.ds.file_meta.PatientID = 'PatientID' with pytest.raises(ValueError, match=r'Only File Meta Information Group ' @@ -1495,11 +1550,12 @@ self.ds.fix_meta_info(enforce_standard=True) def test_validate_and_correct_file_meta(self): - file_meta = Dataset() + file_meta = FileMetaDataset() validate_file_meta(file_meta, enforce_standard=False) with pytest.raises(ValueError): validate_file_meta(file_meta, enforce_standard=True) + file_meta = Dataset() # not FileMetaDataset for bkwds-compat checks file_meta.PatientID = 'PatientID' for enforce_standard in (True, False): with pytest.raises( @@ -1509,7 +1565,7 @@ validate_file_meta( file_meta, enforce_standard=enforce_standard) - file_meta = Dataset() + file_meta = FileMetaDataset() file_meta.MediaStorageSOPClassUID = '1.2.3' file_meta.MediaStorageSOPInstanceUID = '1.2.4' # still missing TransferSyntaxUID @@ -1532,9 +1588,9 @@ assert 'ACME LTD' == file_meta.ImplementationVersionName -class TestFileDataset(object): +class TestFileDataset: def setup(self): - self.test_file = get_testdata_files('CT_small.dcm')[0] + self.test_file = get_testdata_file('CT_small.dcm') def test_pickle(self): ds = pydicom.dcmread(self.test_file) @@ -1554,10 +1610,6 @@ assert ds == ds1 assert ds1.PixelSpacing == [1.0, 1.0] - # Test workaround for python 2 - if compat.in_py2: - ds1.PixelSpacing = ds1.PixelSpacing - ds1.PixelSpacing.insert(1, 2) assert [1, 2, 1] == ds1.PixelSpacing @@ -1582,7 +1634,7 @@ def test_creation_with_container(self): """FileDataset.__init__ works OK with a container such as gzip""" - class Dummy(object): + class Dummy: filename = '/some/path/to/test' ds = Dataset() @@ -1615,7 +1667,7 @@ assert expected_diff == set(dir(di)) - set(dir(ds)) -class TestDatasetOverlayArray(object): +class TestDatasetOverlayArray: """Tests for Dataset.overlay_array().""" def setup(self): """Setup the test datasets and the environment.""" @@ -1623,10 +1675,10 @@ pydicom.config.overlay_data_handlers = [NP_HANDLER] self.ds = dcmread( - get_testdata_files("MR-SIEMENS-DICOM-WithOverlays.dcm")[0] + get_testdata_file("MR-SIEMENS-DICOM-WithOverlays.dcm") ) - class DummyHandler(object): + class DummyHandler: def __init__(self): self.raise_exc = False self.has_dependencies = True @@ -1681,3 +1733,104 @@ pydicom.config.overlay_data_handlers = [self.dummy] with pytest.raises(ValueError, match=r"Dummy error message"): self.ds.overlay_array(0x6000) + + +class TestFileMeta: + def test_deprecation_warning(self): + """Assigning ds.file_meta warns if not FileMetaDataset instance""" + ds = Dataset() + with pytest.warns(DeprecationWarning): + ds.file_meta = Dataset() # not FileMetaDataset + + def test_assign_file_meta(self): + """Test can only set group 2 elements in File Meta""" + # FileMetaDataset accepts only group 2 + file_meta = FileMetaDataset() + with pytest.raises(ValueError): + file_meta.PatientID = "123" + + # No error if assign empty file meta + ds = Dataset() + ds.file_meta = FileMetaDataset() + + # Can assign non-empty file_meta + ds_meta = Dataset() # not FileMetaDataset + ds_meta.TransferSyntaxUID = "1.2" + with pytest.warns(DeprecationWarning): + ds.file_meta = ds_meta + + # Error on assigning file meta if any non-group 2 + ds_meta.PatientName = "x" + with pytest.raises(ValueError): + ds.file_meta = ds_meta + + def test_init_file_meta(self): + """Check instantiation of FileMetaDataset""" + ds_meta = Dataset() + ds_meta.TransferSyntaxUID = "1.2" + + # Accepts with group 2 + file_meta = FileMetaDataset(ds_meta) + assert "1.2" == file_meta.TransferSyntaxUID + + # Accepts dict + dict_meta = {0x20010: DataElement(0x20010, "UI", "2.3")} + file_meta = FileMetaDataset(dict_meta) + assert "2.3" == file_meta.TransferSyntaxUID + + # Fails if not dict or Dataset + with pytest.raises(TypeError): + FileMetaDataset(["1", "2"]) + + # Raises error if init with non-group-2 + ds_meta.PatientName = "x" + with pytest.raises(ValueError): + FileMetaDataset(ds_meta) + + # None can be passed, to match Dataset behavior + FileMetaDataset(None) + + def test_set_file_meta(self): + """Check adding items to existing FileMetaDataset""" + file_meta = FileMetaDataset() + + # Raise error if set non-group 2 + with pytest.raises(ValueError): + file_meta.PatientID = "1" + + # Check assigning via non-Tag + file_meta[0x20010] = DataElement(0x20010, "UI", "2.3") + + # Check RawDataElement + file_meta[0x20010] = RawDataElement( + 0x20010, "UI", 4, "1.23", 0, True, True + ) + + def test_del_file_meta(self): + """Can delete the file_meta attribute""" + ds = Dataset() + ds.file_meta = FileMetaDataset() + del ds.file_meta + assert not hasattr(ds, "file_meta") + + def test_show_file_meta(self): + orig_show = pydicom.config.show_file_meta + pydicom.config.show_file_meta = True + + ds = Dataset() + ds.file_meta = FileMetaDataset() + ds.file_meta.TransferSyntaxUID = "1.2" + ds.PatientName = "test" + shown = str(ds) + + assert shown.startswith("Dataset.file_meta ---") + assert shown.splitlines()[1].startswith( + "(0002, 0010) Transfer Syntax UID" + ) + + # Turn off file_meta display + pydicom.config.show_file_meta = False + shown = str(ds) + assert shown.startswith("(0010, 0010) Patient's Name") + + pydicom.config.show_file_meta = orig_show diff -Nru pydicom-1.4.1/pydicom/tests/test_dicomdir.py pydicom-2.0.0/pydicom/tests/test_dicomdir.py --- pydicom-1.4.1/pydicom/tests/test_dicomdir.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_dicomdir.py 2020-05-29 01:44:31.000000000 +0000 @@ -19,7 +19,7 @@ ) -class TestDicomDir(object): +class TestDicomDir: """Test dicomdir.DicomDir class""" def teardown(self): diff -Nru pydicom-1.4.1/pydicom/tests/test_dictionary.py pydicom-2.0.0/pydicom/tests/test_dictionary.py --- pydicom-1.4.1/pydicom/tests/test_dictionary.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_dictionary.py 2020-05-29 01:44:31.000000000 +0000 @@ -14,7 +14,7 @@ from pydicom.datadict import add_dict_entry, add_dict_entries -class TestDict(object): +class TestDict: def test_tag_not_found(self): """dicom_dictionary: CleanName returns blank string for unknown tag""" assert '' == keyword_for_tag(0x99991111) diff -Nru pydicom-1.4.1/pydicom/tests/test_encaps.py pydicom-2.0.0/pydicom/tests/test_encaps.py --- pydicom-1.4.1/pydicom/tests/test_encaps.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_encaps.py 2020-05-29 01:44:31.000000000 +0000 @@ -24,7 +24,7 @@ JP2K_10FRAME_NOBOT = get_testdata_file('emri_small_jpeg_2k_lossless.dcm') -class TestGetFrameOffsets(object): +class TestGetFrameOffsets: """Test encaps.get_frame_offsets""" def test_bad_tag(self): """Test raises exception if no item tag.""" @@ -92,7 +92,7 @@ get_frame_offsets(fp) -class TestGetNrFragments(object): +class TestGetNrFragments: """Test encaps.get_nr_fragments""" def test_item_undefined_length(self): """Test exception raised if item length undefined.""" @@ -199,7 +199,7 @@ get_nr_fragments(fp) -class TestGeneratePixelDataFragment(object): +class TestGeneratePixelDataFragment: """Test encaps.generate_pixel_data_fragment""" def test_item_undefined_length(self): """Test exception raised if item length undefined.""" @@ -322,7 +322,7 @@ pytest.raises(StopIteration, next, fragments) -class TestGeneratePixelDataFrames(object): +class TestGeneratePixelDataFrames: """Test encaps.generate_pixel_data_frames""" def test_empty_bot_single_fragment(self): """Test a single-frame image where the frame is one fragments""" @@ -483,7 +483,7 @@ next(frame_gen) -class TestGeneratePixelData(object): +class TestGeneratePixelData: """Test encaps.generate_pixel_data""" def test_empty_bot_single_fragment(self): """Test a single-frame image where the frame is one fragments""" @@ -741,7 +741,7 @@ pytest.raises(StopIteration, next, frames) -class TestDecodeDataSequence(object): +class TestDecodeDataSequence: """Test encaps.decode_data_sequence""" def test_empty_bot_single_fragment(self): """Test a single-frame image where the frame is one fragments""" @@ -878,7 +878,7 @@ ] -class TestDefragmentData(object): +class TestDefragmentData: """Test encaps.defragment_data""" def test_defragment(self): """Test joining fragmented data works""" @@ -898,7 +898,7 @@ assert defragment_data(bytestream) == reference -class TestReadItem(object): +class TestReadItem: """Test encaps.read_item""" def test_item_undefined_length(self): """Test exception raised if item length undefined.""" @@ -1009,7 +1009,7 @@ assert read_item(fp) == b'\x01\x02\x03\x04\x05\x06' -class TestFragmentFrame(object): +class TestFragmentFrame: """Test encaps.fragment_frame.""" def test_single_fragment_even_data(self): """Test 1 fragment from even data""" @@ -1095,7 +1095,7 @@ pass -class TestEncapsulateFrame(object): +class TestEncapsulateFrame: """Test encaps.itemise_frame.""" def test_single_item(self): """Test encapsulating into one fragment""" @@ -1133,7 +1133,7 @@ pytest.raises(StopIteration, next, item_generator) -class TestEncapsulate(object): +class TestEncapsulate: """Test encaps.encapsulate.""" def test_encapsulate_single_fragment_per_frame_no_bot(self): """Test encapsulating single fragment per frame with no BOT values.""" diff -Nru pydicom-1.4.1/pydicom/tests/test_env_info.py pydicom-2.0.0/pydicom/tests/test_env_info.py --- pydicom-1.4.1/pydicom/tests/test_env_info.py 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_env_info.py 2020-05-29 01:44:31.000000000 +0000 @@ -0,0 +1,38 @@ +# Copyright 2020 pydicom authors. See LICENSE file for details. +# -*- coding: utf-8 -*- +"""Unit tests for the env_info module.""" + +import pydicom.env_info + + +class TestEnvInfo: + """Test the env_info module""" + + def test_report_looks_like_a_table(self, capsys): + """Test that the report looks like a table""" + pydicom.env_info.main() + + out, err = capsys.readouterr() + table_start = """ +module | version +------ | ------- +platform |""".lstrip() + assert out.startswith(table_start) + + def test_all_modules_reported(self, capsys): + """Test that all modules are reported""" + pydicom.env_info.main() + + out, err = capsys.readouterr() + lines = out.split("\n") + modules = [line.split("|")[0].strip() for line in lines[2:] if line] + + assert modules == [ + "platform", + "Python", + "pydicom", + "gdcm", + "jpeg_ls", + "numpy", + "PIL", + ] diff -Nru pydicom-1.4.1/pydicom/tests/test_environment.py pydicom-2.0.0/pydicom/tests/test_environment.py --- pydicom-1.4.1/pydicom/tests/test_environment.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_environment.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,7 +4,7 @@ The current pydicom testing environments are as follows: * conda: - * Python 2.7: + * Python 3.5, 3.6, 3.7: * no additional packages * numpy * numpy, gdcm (newest and v2.8.4) @@ -12,23 +12,20 @@ * numpy, jpeg-ls * numpy, pillow (jpg, jpg2k), jpeg-ls * numpy, pillow (jpg, jpg2k), jpeg-ls, gdcm - * Python 3.4, 3.5, 3.6, 3.7: * As with 2.7 - * Python 2.7, 3.7: + * Python 3.7: * numpy, pillow (jpg) * pypy - * Python 2.7, 3.5: + * Python 3.5: * no additional packages * numpy * ubuntu - * Python 2.7: - * no additional packages - * numpy + Environmental variables ----------------------- DISTRIB: conda, pypy, ubuntu -PYTHON_VERSION: 2.7, 3.4, 3.5, 3.6, 3.7 +PYTHON_VERSION: 3.5, 3.6, 3.7 NUMPY: true, false PILLOW: jpeg, both, false JPEG_LS: false, true @@ -64,7 +61,7 @@ @pytest.mark.skipif(not IN_TRAVIS, reason="Tests not running in Travis") -class TestBuilds(object): +class TestBuilds: """Tests for the testing builds in Travis CI.""" def test_distribution(self): """Test that the distribution is correct.""" diff -Nru pydicom-1.4.1/pydicom/tests/test_filebase.py pydicom-2.0.0/pydicom/tests/test_filebase.py --- pydicom-1.4.1/pydicom/tests/test_filebase.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_filebase.py 2020-05-29 01:44:31.000000000 +0000 @@ -13,7 +13,7 @@ TEST_FILE = get_testdata_files('CT_small.dcm')[0] -class TestDicomIO(object): +class TestDicomIO: """Test filebase.DicomIO class""" def test_init(self): """Test __init__""" @@ -207,7 +207,7 @@ assert not fp.is_implicit_VR -class TestDicomFileLike(object): +class TestDicomFileLike: """Test filebase.DicomFileLike class""" def test_init_good_parent(self): """Test methods are set OK if parent is good""" @@ -246,7 +246,7 @@ assert fp.parent_read(2) == b'\x00\x01' -class TestDicomBytesIO(object): +class TestDicomBytesIO: """Test filebase.DicomBytesIO class""" def test_getvalue(self): """Test DicomBytesIO.getvalue""" @@ -254,7 +254,7 @@ assert fp.getvalue() == b'\x00\x01\x00\x02' -class TestDicomFile(object): +class TestDicomFile: """Test filebase.DicomFile() function""" def test_read(self): """Test the function""" diff -Nru pydicom-1.4.1/pydicom/tests/test_filereader.py pydicom-2.0.0/pydicom/tests/test_filereader.py --- pydicom-1.4.1/pydicom/tests/test_filereader.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_filereader.py 2020-05-29 01:44:31.000000000 +0000 @@ -7,6 +7,7 @@ from io import BytesIO import os import shutil +from pathlib import Path from struct import unpack import sys import tempfile @@ -15,27 +16,29 @@ import pydicom.config from pydicom import config -from pydicom.dataset import Dataset, FileDataset -from pydicom.data import get_testdata_files +from pydicom.dataset import Dataset, FileDataset, FileMetaDataset +from pydicom.data import get_testdata_file from pydicom.datadict import add_dict_entries from pydicom.filereader import dcmread, read_dataset from pydicom.dataelem import DataElement, DataElement_from_raw from pydicom.errors import InvalidDicomError from pydicom.filebase import DicomBytesIO from pydicom.filereader import data_element_generator +from pydicom.multival import MultiValue from pydicom.sequence import Sequence from pydicom.tag import Tag, TupleTag from pydicom.uid import ImplicitVRLittleEndian import pydicom.valuerep +from pydicom import values from pydicom.pixel_data_handlers import gdcm_handler + have_gdcm_handler = gdcm_handler.is_available() -try: +have_numpy = pydicom.config.have_numpy +if have_numpy: import numpy # NOQA -except ImportError: - numpy = None try: import jpeg_ls @@ -52,54 +55,49 @@ # Neither worked, so it's likely not installed. PILImg = None -have_numpy = numpy is not None have_jpeg_ls = jpeg_ls is not None have_pillow = PILImg is not None -empty_number_tags_name = get_testdata_files( - "reportsi_with_empty_number_tags.dcm")[0] -rtplan_name = get_testdata_files("rtplan.dcm")[0] -rtdose_name = get_testdata_files("rtdose.dcm")[0] -ct_name = get_testdata_files("CT_small.dcm")[0] -mr_name = get_testdata_files("MR_small.dcm")[0] -truncated_mr_name = get_testdata_files("MR_truncated.dcm")[0] -jpeg2000_name = get_testdata_files("JPEG2000.dcm")[0] -jpeg2000_lossless_name = get_testdata_files("MR_small_jp2klossless.dcm")[0] -jpeg_ls_lossless_name = get_testdata_files("MR_small_jpeg_ls_lossless.dcm")[0] -jpeg_lossy_name = get_testdata_files("JPEG-lossy.dcm")[0] -jpeg_lossless_name = get_testdata_files("JPEG-LL.dcm")[0] -deflate_name = get_testdata_files("image_dfl.dcm")[0] -rtstruct_name = get_testdata_files("rtstruct.dcm")[0] -priv_SQ_name = get_testdata_files("priv_SQ.dcm") -# be sure that we don't pick up the nested_priv_sq -priv_SQ_name = [filename - for filename in priv_SQ_name - if 'nested' not in filename] -priv_SQ_name = priv_SQ_name[0] -nested_priv_SQ_name = get_testdata_files("nested_priv_SQ.dcm")[0] -meta_missing_tsyntax_name = get_testdata_files("meta_missing_tsyntax.dcm")[0] -no_meta_group_length = get_testdata_files("no_meta_group_length.dcm")[0] -gzip_name = get_testdata_files("zipMR.gz")[0] -color_px_name = get_testdata_files("color-px.dcm")[0] -color_pl_name = get_testdata_files("color-pl.dcm")[0] -explicit_vr_le_no_meta = get_testdata_files("ExplVR_LitEndNoMeta.dcm")[0] -explicit_vr_be_no_meta = get_testdata_files("ExplVR_BigEndNoMeta.dcm")[0] -emri_name = get_testdata_files("emri_small.dcm")[0] -emri_big_endian_name = get_testdata_files("emri_small_big_endian.dcm")[0] -emri_jpeg_ls_lossless = get_testdata_files( - "emri_small_jpeg_ls_lossless.dcm")[0] -emri_jpeg_2k_lossless = get_testdata_files( - "emri_small_jpeg_2k_lossless.dcm")[0] -emri_jpeg_2k_lossless_too_short = get_testdata_files( - "emri_small_jpeg_2k_lossless_too_short.dcm")[0] -color_3d_jpeg_baseline = get_testdata_files("color3d_jpeg_baseline.dcm")[0] +empty_number_tags_name = get_testdata_file( + "reportsi_with_empty_number_tags.dcm" +) +rtplan_name = get_testdata_file("rtplan.dcm") +rtdose_name = get_testdata_file("rtdose.dcm") +ct_name = get_testdata_file("CT_small.dcm") +mr_name = get_testdata_file("MR_small.dcm") +truncated_mr_name = get_testdata_file("MR_truncated.dcm") +jpeg2000_name = get_testdata_file("JPEG2000.dcm") +jpeg2000_lossless_name = get_testdata_file("MR_small_jp2klossless.dcm") +jpeg_ls_lossless_name = get_testdata_file("MR_small_jpeg_ls_lossless.dcm") +jpeg_lossy_name = get_testdata_file("JPEG-lossy.dcm") +jpeg_lossless_name = get_testdata_file("JPEG-LL.dcm") +deflate_name = get_testdata_file("image_dfl.dcm") +rtstruct_name = get_testdata_file("rtstruct.dcm") +priv_SQ_name = get_testdata_file("priv_SQ.dcm") +nested_priv_SQ_name = get_testdata_file("nested_priv_SQ.dcm") +meta_missing_tsyntax_name = get_testdata_file("meta_missing_tsyntax.dcm") +no_meta_group_length = get_testdata_file("no_meta_group_length.dcm") +gzip_name = get_testdata_file("zipMR.gz") +color_px_name = get_testdata_file("color-px.dcm") +color_pl_name = get_testdata_file("color-pl.dcm") +explicit_vr_le_no_meta = get_testdata_file("ExplVR_LitEndNoMeta.dcm") +explicit_vr_be_no_meta = get_testdata_file("ExplVR_BigEndNoMeta.dcm") +emri_name = get_testdata_file("emri_small.dcm") +emri_big_endian_name = get_testdata_file("emri_small_big_endian.dcm") +emri_jpeg_ls_lossless = get_testdata_file("emri_small_jpeg_ls_lossless.dcm") +emri_jpeg_2k_lossless = get_testdata_file("emri_small_jpeg_2k_lossless.dcm") +emri_jpeg_2k_lossless_too_short = get_testdata_file( + "emri_small_jpeg_2k_lossless_too_short.dcm" +) +color_3d_jpeg_baseline = get_testdata_file("color3d_jpeg_baseline.dcm") dir_name = os.path.dirname(sys.argv[0]) save_dir = os.getcwd() -class TestReader(object): +class TestReader: def teardown(self): config.enforce_valid_values = False + config.replace_un_with_known_vr = True def test_empty_numbers_tag(self): """Test that an empty tag with a number VR (FL, UL, SL, US, @@ -120,44 +118,52 @@ os.remove(utf8_filename) assert ds is not None + def test_pathlib_path_filename(self): + """Check that file can be read using pathlib.Path""" + ds = dcmread(Path(priv_SQ_name)) + def test_RTPlan(self): """Returns correct values for sample data elements in test RT Plan file. """ + orig_use_numpy = config.use_DS_numpy + config.use_DS_numpy = False plan = dcmread(rtplan_name) beam = plan.BeamSequence[0] # if not two controlpoints, then this would raise exception cp0, cp1 = beam.ControlPointSequence assert "unit001" == beam.TreatmentMachineName - assert beam[0x300a, 0x00b2].value == beam.TreatmentMachineName + assert beam[0x300A, 0x00B2].value == beam.TreatmentMachineName got = cp1.ReferencedDoseReferenceSequence[ - 0].CumulativeDoseReferenceCoefficient + 0 + ].CumulativeDoseReferenceCoefficient DS = pydicom.valuerep.DS - expected = DS('0.9990268') + expected = DS("0.9990268") assert expected == got got = cp0.BeamLimitingDevicePositionSequence[0].LeafJawPositions - assert [DS('-100'), DS('100.0')] == got + assert [DS("-100"), DS("100.0")] == got + config.use_DS_numpy = orig_use_numpy def test_RTDose(self): """Returns correct values for sample data elements in test RT Dose file""" dose = dcmread(rtdose_name) - assert Tag((0x3004, 0x000c)) == dose.FrameIncrementPointer + assert Tag((0x3004, 0x000C)) == dose.FrameIncrementPointer assert dose[0x28, 9].value == dose.FrameIncrementPointer # try a value that is nested the deepest # (so deep I break it into two steps!) - fract = ( - dose.ReferencedRTPlanSequence[0].ReferencedFractionGroupSequence[0] - ) + fract = dose.ReferencedRTPlanSequence[ + 0 + ].ReferencedFractionGroupSequence[0] assert 1 == fract.ReferencedBeamSequence[0].ReferencedBeamNumber def test_CT(self): """Returns correct values for sample data elements in test CT file.""" ct = dcmread(ct_name) - assert '1.3.6.1.4.1.5962.2' == ct.file_meta.ImplementationClassUID + assert "1.3.6.1.4.1.5962.2" == ct.file_meta.ImplementationClassUID value = ct.file_meta[0x2, 0x12].value assert value == ct.file_meta.ImplementationClassUID @@ -165,8 +171,12 @@ # [-158.13580300000001, -179.035797, -75.699996999999996] got = ct.ImagePositionPatient DS = pydicom.valuerep.DS - expected = [DS('-158.135803'), DS('-179.035797'), DS('-75.699997')] - assert expected == got + if have_numpy and config.use_DS_numpy: + expected = numpy.array([-158.135803, -179.035797, -75.699997]) + assert numpy.allclose(got, expected) + else: + expected = [DS("-158.135803"), DS("-179.035797"), DS("-75.699997")] + assert got == expected assert 128 == ct.Rows assert 128 == ct.Columns @@ -174,7 +184,7 @@ assert 128 * 128 * 2 == len(ct.PixelData) # Also test private elements name can be resolved: - got = ct[(0x0043, 0x104e)].name + got = ct[(0x0043, 0x104E)].name assert "[Duration of X-ray on]" == got @pytest.mark.skipif(not have_numpy, reason="Numpy not installed") @@ -207,7 +217,7 @@ obs_seq0 = rtss.RTROIObservationsSequence[0] got = obs_seq0.ROIPhysicalPropertiesSequence[0].ROIPhysicalProperty - assert 'REL_ELEC_DENSITY' == got + assert "REL_ELEC_DENSITY" == got def test_dir(self): """Returns correct dir attributes for both Dataset and DICOM names @@ -216,16 +226,24 @@ rtss = dcmread(rtstruct_name, force=True) # sample some expected 'dir' values got_dir = dir(rtss) - expect_in_dir = ['pixel_array', 'add_new', 'ROIContourSequence', - 'StructureSetDate'] + expect_in_dir = [ + "pixel_array", + "add_new", + "ROIContourSequence", + "StructureSetDate", + ] for name in expect_in_dir: assert name in got_dir # Now check for some items in dir() of a nested item roi0 = rtss.ROIContourSequence[0] got_dir = dir(roi0) - expect_in_dir = ['pixel_array', 'add_new', 'ReferencedROINumber', - 'ROIDisplayColor'] + expect_in_dir = [ + "pixel_array", + "add_new", + "ReferencedROINumber", + "ROIDisplayColor", + ] for name in expect_in_dir: assert name in got_dir @@ -234,11 +252,16 @@ mr = dcmread(mr_name) # (0010, 0010) Patient's Name 'CompressedSamples^MR1' mr.decode() - assert 'CompressedSamples^MR1' == mr.PatientName + assert "CompressedSamples^MR1" == mr.PatientName assert mr[0x10, 0x10].value == mr.PatientName DS = pydicom.valuerep.DS - assert [DS('0.3125'), DS('0.3125')] == mr.PixelSpacing + + if have_numpy and config.use_DS_numpy: + expected = numpy.array([0.3125, 0.3125]) + assert numpy.allclose(mr.PixelSpacing, expected) + else: + assert [DS("0.3125"), DS("0.3125")] == mr.PixelSpacing def test_deflate(self): """Returns correct values for sample data elements in test compressed @@ -249,6 +272,17 @@ ds = dcmread(deflate_name) assert "WSD" == ds.ConversionType + def test_bad_sequence(self): + """Test that automatic UN conversion can be switched off.""" + with pytest.raises(NotImplementedError): + ds = dcmread(get_testdata_file("bad_sequence.dcm")) + # accessing the elements of the faulty sequence raises + str(ds.CTDIPhantomTypeCodeSequence) + + config.replace_un_with_known_vr = False + ds = dcmread(get_testdata_file("bad_sequence.dcm")) + str(ds.CTDIPhantomTypeCodeSequence) + def test_no_pixels_read(self): """Returns all data elements before pixels using stop_before_pixels=False. @@ -258,43 +292,52 @@ ctpartial_tags = sorted(ctpartial.keys()) ctfull = dcmread(ct_name) ctfull_tags = sorted(ctfull.keys()) - missing = [Tag(0x7fe0, 0x10), Tag(0xfffc, 0xfffc)] + missing = [Tag(0x7FE0, 0x10), Tag(0xFFFC, 0xFFFC)] assert ctfull_tags == ctpartial_tags + missing def test_specific_tags(self): """Returns only tags specified by user.""" - ctspecific = dcmread(ct_name, specific_tags=[ - Tag(0x0010, 0x0010), 'PatientID', 'ImageType', 'ViewName']) + ctspecific = dcmread( + ct_name, + specific_tags=[ + Tag(0x0010, 0x0010), + "PatientID", + "ImageType", + "ViewName", + ], + ) ctspecific_tags = sorted(ctspecific.keys()) expected = [ # SpecificCharacterSet is always added # ViewName does not exist in the data set - Tag(0x0008, 0x0005), Tag(0x0008, 0x0008), - Tag(0x0010, 0x0010), Tag(0x0010, 0x0020) + Tag(0x0008, 0x0005), + Tag(0x0008, 0x0008), + Tag(0x0010, 0x0010), + Tag(0x0010, 0x0020), ] assert expected == ctspecific_tags def test_specific_tags_with_unknown_length_SQ(self): """Returns only tags specified by user.""" - unknown_len_sq_tag = Tag(0x3f03, 0x1001) + unknown_len_sq_tag = Tag(0x3F03, 0x1001) tags = dcmread(priv_SQ_name, specific_tags=[unknown_len_sq_tag]) tags = sorted(tags.keys()) assert [unknown_len_sq_tag] == tags - tags = dcmread(priv_SQ_name, specific_tags=['PatientName']) + tags = dcmread(priv_SQ_name, specific_tags=["PatientName"]) tags = sorted(tags.keys()) assert [] == tags def test_specific_tags_with_unknown_length_tag(self): """Returns only tags specified by user.""" - unknown_len_tag = Tag(0x7fe0, 0x0010) # Pixel Data + unknown_len_tag = Tag(0x7FE0, 0x0010) # Pixel Data tags = dcmread(emri_jpeg_2k_lossless, specific_tags=[unknown_len_tag]) tags = sorted(tags.keys()) # SpecificCharacterSet is always added assert [Tag(0x08, 0x05), unknown_len_tag] == tags tags = dcmread( - emri_jpeg_2k_lossless, specific_tags=['SpecificCharacterSet'] + emri_jpeg_2k_lossless, specific_tags=["SpecificCharacterSet"] ) tags = sorted(tags.keys()) assert [Tag(0x08, 0x05)] == tags @@ -303,15 +346,19 @@ """Tests handling of incomplete sequence value.""" # the data set is the same as emri_jpeg_2k_lossless, # with the last 8 bytes removed to provoke the EOF error - unknown_len_tag = Tag(0x7fe0, 0x0010) # Pixel Data - with pytest.warns(UserWarning, match='End of file reached*'): - dcmread(emri_jpeg_2k_lossless_too_short, - specific_tags=[unknown_len_tag]) + unknown_len_tag = Tag(0x7FE0, 0x0010) # Pixel Data + with pytest.warns(UserWarning, match="End of file reached*"): + dcmread( + emri_jpeg_2k_lossless_too_short, + specific_tags=[unknown_len_tag], + ) config.enforce_valid_values = True - with pytest.raises(EOFError, match='End of file reached*'): - dcmread(emri_jpeg_2k_lossless_too_short, - specific_tags=[unknown_len_tag]) + with pytest.raises(EOFError, match="End of file reached*"): + dcmread( + emri_jpeg_2k_lossless_too_short, + specific_tags=[unknown_len_tag], + ) def test_private_SQ(self): """Can read private undefined length SQ without error.""" @@ -334,21 +381,21 @@ ds = dcmread(nested_priv_SQ_name) # Make sure that the entire dataset was read in - pixel_data_tag = TupleTag((0x7fe0, 0x10)) + pixel_data_tag = TupleTag((0x7FE0, 0x10)) assert pixel_data_tag in ds # Check that the DataElement is indeed a Sequence tag = TupleTag((0x01, 0x01)) seq0 = ds[tag] - assert 'SQ' == seq0.VR + assert "SQ" == seq0.VR # Now verify the presence of the nested private SQ seq1 = seq0[0][tag] - assert 'SQ' == seq1.VR + assert "SQ" == seq1.VR # Now make sure the values that are parsed are correct - assert b'Double Nested SQ' == seq1[0][tag].value - assert b'Nested SQ' == seq0[0][0x01, 0x02].value + assert b"Double Nested SQ" == seq1[0][tag].value + assert b"Nested SQ" == seq0[0][0x01, 0x02].value def test_no_meta_group_length(self): """Read file with no group length in file meta.""" @@ -366,7 +413,7 @@ # Repeat one test from nested private sequence test to maker sure # file was read correctly - pixel_data_tag = TupleTag((0x7fe0, 0x10)) + pixel_data_tag = TupleTag((0x7FE0, 0x10)) assert pixel_data_tag in ds def test_explicit_VR_little_endian_no_meta(self): @@ -397,8 +444,8 @@ """Test correcting ambiguous VR elements read from file""" ds = Dataset() ds.PixelRepresentation = 0 - ds.add(DataElement(0x00280108, 'US', 10)) - ds.add(DataElement(0x00280109, 'US', 500)) + ds.add(DataElement(0x00280108, "US", 10)) + ds.add(DataElement(0x00280109, "US", 500)) fp = BytesIO() file_ds = FileDataset(fp, ds) @@ -407,15 +454,15 @@ file_ds.save_as(fp, write_like_original=True) ds = dcmread(fp, force=True) - assert 'US' == ds[0x00280108].VR + assert "US" == ds[0x00280108].VR assert 10 == ds.SmallestPixelValueInSeries def test_correct_ambiguous_explicit_vr(self): """Test correcting ambiguous VR elements read from file""" ds = Dataset() ds.PixelRepresentation = 0 - ds.add(DataElement(0x00280108, 'US', 10)) - ds.add(DataElement(0x00280109, 'US', 500)) + ds.add(DataElement(0x00280108, "US", 10)) + ds.add(DataElement(0x00280109, "US", 500)) fp = BytesIO() file_ds = FileDataset(fp, ds) @@ -424,7 +471,7 @@ file_ds.save_as(fp, write_like_original=True) ds = dcmread(fp, force=True) - assert 'US' == ds[0x00280108].VR + assert "US" == ds[0x00280108].VR assert 10 == ds.SmallestPixelValueInSeries def test_correct_ambiguous_vr_compressed(self): @@ -438,15 +485,15 @@ file_ds.save_as(fp, write_like_original=True) ds = dcmread(fp, force=True) - assert 'OB' == ds[0x7fe00010].VR + assert "OB" == ds[0x7FE00010].VR def test_long_specific_char_set(self): """Test that specific character set is read even if it is longer than defer_size""" ds = Dataset() - long_specific_char_set_value = ['ISO 2022IR 100'] * 9 - ds.add(DataElement(0x00080005, 'CS', long_specific_char_set_value)) + long_specific_char_set_value = ["ISO 2022IR 100"] * 9 + ds.add(DataElement(0x00080005, "CS", long_specific_char_set_value)) msg = ( r"Unknown encoding 'ISO 2022IR 100' - using default encoding " @@ -464,37 +511,41 @@ def test_no_preamble_file_meta_dataset(self): """Test correct read of group 2 elements with no preamble.""" - bytestream = (b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00\x20\x20\x10\x00' - b'\x02\x00\x00\x00\x01\x00\x20\x20\x20\x00\x06\x00' - b'\x00\x00\x4e\x4f\x52\x4d\x41\x4c') + bytestream = ( + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00\x20\x20\x10\x00" + b"\x02\x00\x00\x00\x01\x00\x20\x20\x20\x00\x06\x00" + b"\x00\x00\x4e\x4f\x52\x4d\x41\x4c" + ) fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'MediaStorageSOPClassUID' in ds.file_meta + assert "MediaStorageSOPClassUID" in ds.file_meta assert ImplicitVRLittleEndian == ds.file_meta.TransferSyntaxUID - assert 'NORMAL' == ds.Polarity + assert "NORMAL" == ds.Polarity assert 1 == ds.ImageBoxPosition def test_no_preamble_command_group_dataset(self): """Test correct read of group 0 and 2 elements with no preamble.""" - bytestream = (b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00' - b'\x20\x20\x10\x00\x02\x00\x00\x00\x01\x00\x20\x20' - b'\x20\x00\x06\x00\x00\x00\x4e\x4f\x52\x4d\x41\x4c' - b'\x00\x00\x10\x01\x02\x00\x00\x00\x03\x00') + bytestream = ( + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00" + b"\x20\x20\x10\x00\x02\x00\x00\x00\x01\x00\x20\x20" + b"\x20\x00\x06\x00\x00\x00\x4e\x4f\x52\x4d\x41\x4c" + b"\x00\x00\x10\x01\x02\x00\x00\x00\x03\x00" + ) fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'MediaStorageSOPClassUID' in ds.file_meta + assert "MediaStorageSOPClassUID" in ds.file_meta assert ImplicitVRLittleEndian == ds.file_meta.TransferSyntaxUID - assert 'NORMAL' == ds.Polarity + assert "NORMAL" == ds.Polarity assert 1 == ds.ImageBoxPosition assert 3 == ds.MessageID @@ -502,127 +553,143 @@ """Test file is read correctly even if FileMetaInformationGroupLength is incorrect. """ - bytestream = (b'\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00' - b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00' - b'\x20\x20\x10\x00\x02\x00\x00\x00\x01\x00\x20\x20' - b'\x20\x00\x06\x00\x00\x00\x4e\x4f\x52\x4d\x41\x4c') + bytestream = ( + b"\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00" + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00" + b"\x20\x20\x10\x00\x02\x00\x00\x00\x01\x00\x20\x20" + b"\x20\x00\x06\x00\x00\x00\x4e\x4f\x52\x4d\x41\x4c" + ) fp = BytesIO(bytestream) ds = dcmread(fp, force=True) value = ds.file_meta.FileMetaInformationGroupLength assert not len(bytestream) - 12 == value assert 10 == ds.file_meta.FileMetaInformationGroupLength - assert 'MediaStorageSOPClassUID' in ds.file_meta + assert "MediaStorageSOPClassUID" in ds.file_meta assert ImplicitVRLittleEndian == ds.file_meta.TransferSyntaxUID - assert 'NORMAL' == ds.Polarity + assert "NORMAL" == ds.Polarity assert 1 == ds.ImageBoxPosition def test_preamble_command_meta_no_dataset(self): """Test reading only preamble, command and meta elements""" - preamble = b'\x00' * 128 - prefix = b'DICM' - command = (b'\x00\x00\x00\x00\x04\x00\x00\x00\x38' - b'\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00' - b'\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31' - b'\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00' - b'\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00' - b'\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00' - b'\x00\x00\x08\x02\x00\x00\x00\x01\x01') - meta = (b'\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00' - b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00') + preamble = b"\x00" * 128 + prefix = b"DICM" + command = ( + b"\x00\x00\x00\x00\x04\x00\x00\x00\x38" + b"\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00" + b"\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31" + b"\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00" + b"\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00" + b"\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00" + b"\x00\x00\x08\x02\x00\x00\x00\x01\x01" + ) + meta = ( + b"\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00" + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00" + ) bytestream = preamble + prefix + meta + command fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'TransferSyntaxUID' in ds.file_meta - assert 'MessageID' in ds + assert "TransferSyntaxUID" in ds.file_meta + assert "MessageID" in ds def test_preamble_meta_no_dataset(self): """Test reading only preamble and meta elements""" - preamble = b'\x00' * 128 - prefix = b'DICM' - meta = (b'\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00' - b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00') + preamble = b"\x00" * 128 + prefix = b"DICM" + meta = ( + b"\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00" + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00" + ) bytestream = preamble + prefix + meta fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert b'\x00' * 128 == ds.preamble - assert 'TransferSyntaxUID' in ds.file_meta + assert b"\x00" * 128 == ds.preamble + assert "TransferSyntaxUID" in ds.file_meta assert Dataset() == ds[:] def test_preamble_commandset_no_dataset(self): """Test reading only preamble and command set""" - preamble = b'\x00' * 128 - prefix = b'DICM' - command = (b'\x00\x00\x00\x00\x04\x00\x00\x00\x38' - b'\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00' - b'\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31' - b'\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00' - b'\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00' - b'\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00' - b'\x00\x00\x08\x02\x00\x00\x00\x01\x01') + preamble = b"\x00" * 128 + prefix = b"DICM" + command = ( + b"\x00\x00\x00\x00\x04\x00\x00\x00\x38" + b"\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00" + b"\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31" + b"\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00" + b"\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00" + b"\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00" + b"\x00\x00\x08\x02\x00\x00\x00\x01\x01" + ) bytestream = preamble + prefix + command fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'MessageID' in ds + assert "MessageID" in ds assert Dataset() == ds.file_meta def test_meta_no_dataset(self): """Test reading only meta elements""" - bytestream = (b'\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00' - b'\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e' - b'\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e' - b'\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49' - b'\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30' - b'\x30\x30\x38\x2e\x31\x2e\x32\x00') + bytestream = ( + b"\x02\x00\x00\x00\x55\x4C\x04\x00\x0A\x00\x00\x00" + b"\x02\x00\x02\x00\x55\x49\x16\x00\x31\x2e\x32\x2e" + b"\x38\x34\x30\x2e\x31\x30\x30\x30\x38\x2e\x35\x2e" + b"\x31\x2e\x31\x2e\x39\x00\x02\x00\x10\x00\x55\x49" + b"\x12\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31\x30" + b"\x30\x30\x38\x2e\x31\x2e\x32\x00" + ) fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'TransferSyntaxUID' in ds.file_meta + assert "TransferSyntaxUID" in ds.file_meta assert Dataset() == ds[:] def test_commandset_no_dataset(self): """Test reading only command set elements""" - bytestream = (b'\x00\x00\x00\x00\x04\x00\x00\x00\x38' - b'\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00' - b'\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31' - b'\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00' - b'\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00' - b'\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00' - b'\x00\x00\x08\x02\x00\x00\x00\x01\x01') + bytestream = ( + b"\x00\x00\x00\x00\x04\x00\x00\x00\x38" + b"\x00\x00\x00\x00\x00\x02\x00\x12\x00\x00" + b"\x00\x31\x2e\x32\x2e\x38\x34\x30\x2e\x31" + b"\x30\x30\x30\x38\x2e\x31\x2e\x31\x00\x00" + b"\x00\x00\x01\x02\x00\x00\x00\x30\x00\x00" + b"\x00\x10\x01\x02\x00\x00\x00\x07\x00\x00" + b"\x00\x00\x08\x02\x00\x00\x00\x01\x01" + ) fp = BytesIO(bytestream) ds = dcmread(fp, force=True) - assert 'MessageID' in ds + assert "MessageID" in ds assert ds.preamble is None assert Dataset() == ds.file_meta def test_file_meta_dataset_implicit_vr(self): """Test reading a file meta dataset that is implicit VR""" - bytestream = (b'\x02\x00\x10\x00\x12\x00\x00\x00' - b'\x31\x2e\x32\x2e\x38\x34\x30\x2e' - b'\x31\x30\x30\x30\x38\x2e\x31\x2e' - b'\x32\x00') + bytestream = ( + b"\x02\x00\x10\x00\x12\x00\x00\x00" + b"\x31\x2e\x32\x2e\x38\x34\x30\x2e" + b"\x31\x30\x30\x30\x38\x2e\x31\x2e" + b"\x32\x00" + ) fp = BytesIO(bytestream) with pytest.warns(UserWarning): ds = dcmread(fp, force=True) - assert 'TransferSyntaxUID' in ds.file_meta + assert "TransferSyntaxUID" in ds.file_meta def test_no_dataset(self): """Test reading no elements or preamble produces empty Dataset""" - bytestream = b'' + bytestream = b"" fp = BytesIO(bytestream) ds = dcmread(fp, force=True) assert ds.preamble is None @@ -637,6 +704,12 @@ assert Dataset() == ds.file_meta assert Dataset() == ds[:] + def test_empty_specific_character_set(self): + """Test that an empty Specific Character Set is handled correctly. + Regression test for #1038""" + ds = dcmread(get_testdata_file("empty_charset_LEI.dcm")) + assert ds.read_encoding == ["iso8859"] + def test_dcmread_does_not_raise(self): """Test that reading from DicomBytesIO does not raise on EOF. Regression test for #358.""" @@ -651,60 +724,99 @@ except StopIteration: pass except EOFError: - self.fail('Unexpected EOFError raised') + self.fail("Unexpected EOFError raised") def test_lut_descriptor(self): """Regression test for #942: incorrect first value""" prefixes = [ - b'\x28\x00\x01\x11', - b'\x28\x00\x02\x11', - b'\x28\x00\x03\x11', - b'\x28\x00\x02\x30' + b"\x28\x00\x01\x11", + b"\x28\x00\x02\x11", + b"\x28\x00\x03\x11", + b"\x28\x00\x02\x30", ] - suffix = b'\x53\x53\x06\x00\x00\xf5\x00\xf8\x10\x00' + suffix = b"\x53\x53\x06\x00\x00\xf5\x00\xf8\x10\x00" for raw_tag in prefixes: - tag = unpack('<2H', raw_tag) + tag = unpack("<2H", raw_tag) bs = DicomBytesIO(raw_tag + suffix) bs.is_little_endian = True bs.is_implicit_VR = False ds = dcmread(bs, force=True) elem = ds[tag] - assert elem.VR == 'SS' + assert elem.VR == "SS" assert elem.value == [62720, -2048, 16] + def test_lut_descriptor_empty(self): + """Regression test for #1049: LUT empty raises.""" + bs = DicomBytesIO(b"\x28\x00\x01\x11\x53\x53\x00\x00") + bs.is_little_endian = True + bs.is_implicit_VR = False + ds = dcmread(bs, force=True) + elem = ds[0x00281101] + assert elem.value is None + assert elem.VR == "SS" + + def test_lut_descriptor_singleton(self): + """Test LUT Descriptor with VM = 1""" + bs = DicomBytesIO(b"\x28\x00\x01\x11\x53\x53\x02\x00\x00\xf5") + bs.is_little_endian = True + bs.is_implicit_VR = False + ds = dcmread(bs, force=True) + elem = ds[0x00281101] + # No conversion to US if not a triplet + assert elem.value == -2816 + assert elem.VR == "SS" + + def test_reading_of(self): + """Test reading a dataset with OF element.""" + bs = DicomBytesIO( + b"\x28\x00\x01\x11\x53\x53\x06\x00\x00\xf5\x00\xf8\x10\x00" + b"\xe0\x7f\x08\x00\x4F\x46\x00\x00\x04\x00\x00\x00\x00\x01\x02\x03" + ) + bs.is_little_endian = True + bs.is_implicit_VR = False + + ds = dcmread(bs, force=True) + elem = ds["FloatPixelData"] + assert "OF" == elem.VR + assert b"\x00\x01\x02\x03" == elem.value + -class TestIncorrectVR(object): +class TestIncorrectVR: def setup(self): config.enforce_valid_values = False self.ds_explicit = BytesIO( - b'\x08\x00\x05\x00CS\x0a\x00ISO_IR 100' # SpecificCharacterSet - b'\x08\x00\x20\x00DA\x08\x0020000101' # StudyDate + b"\x08\x00\x05\x00CS\x0a\x00ISO_IR 100" # SpecificCharacterSet + b"\x08\x00\x20\x00DA\x08\x0020000101" # StudyDate ) self.ds_implicit = BytesIO( - b'\x08\x00\x05\x00\x0a\x00\x00\x00ISO_IR 100' - b'\x08\x00\x20\x00\x08\x00\x00\x0020000101' + b"\x08\x00\x05\x00\x0a\x00\x00\x00ISO_IR 100" + b"\x08\x00\x20\x00\x08\x00\x00\x0020000101" ) def teardown(self): config.enforce_valid_values = False def test_implicit_vr_expected_explicit_used(self): - msg = ('Expected implicit VR, but found explicit VR - ' - 'using explicit VR for reading') + msg = ( + "Expected implicit VR, but found explicit VR - " + "using explicit VR for reading" + ) with pytest.warns(UserWarning, match=msg): ds = read_dataset( self.ds_explicit, is_implicit_VR=True, is_little_endian=True ) - assert 'ISO_IR 100' == ds.SpecificCharacterSet - assert '20000101' == ds.StudyDate + assert "ISO_IR 100" == ds.SpecificCharacterSet + assert "20000101" == ds.StudyDate def test_implicit_vr_expected_explicit_used_strict(self): config.enforce_valid_values = True - msg = ('Expected implicit VR, but found explicit VR - ' - 'using explicit VR for reading') + msg = ( + "Expected implicit VR, but found explicit VR - " + "using explicit VR for reading" + ) with pytest.raises(InvalidDicomError, match=msg): read_dataset( @@ -712,20 +824,24 @@ ) def test_explicit_vr_expected_implicit_used(self): - msg = ('Expected explicit VR, but found implicit VR - ' - 'using implicit VR for reading') + msg = ( + "Expected explicit VR, but found implicit VR - " + "using implicit VR for reading" + ) with pytest.warns(UserWarning, match=msg): ds = read_dataset( self.ds_implicit, is_implicit_VR=False, is_little_endian=True ) - assert 'ISO_IR 100' == ds.SpecificCharacterSet - assert '20000101' == ds.StudyDate + assert "ISO_IR 100" == ds.SpecificCharacterSet + assert "20000101" == ds.StudyDate def test_explicit_vr_expected_implicit_used_strict(self): config.enforce_valid_values = True - msg = ('Expected explicit VR, but found implicit VR - ' - 'using implicit VR for reading') + msg = ( + "Expected explicit VR, but found implicit VR - " + "using implicit VR for reading" + ) with pytest.raises(InvalidDicomError, match=msg): read_dataset( self.ds_implicit, is_implicit_VR=False, is_little_endian=True @@ -738,12 +854,12 @@ # followed by a sequence with an item (dataset) having # a data element length that looks like a potential valid VR ds = Dataset() - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.MediaStorageSOPClassUID = "1.1.1" ds.file_meta.MediaStorageSOPInstanceUID = "2.2.2" ds.is_implicit_VR = True ds.is_little_endian = True - ds.SOPClassUID = '9.9.9' # First item group 8 in top-level dataset + ds.SOPClassUID = "9.9.9" # First item group 8 in top-level dataset seq = Sequence() seq_ds = Dataset() seq_ds.BadPixelImage = b"\3" * 0x5244 # length looks like "DR" @@ -761,48 +877,48 @@ ds.remove_private_tags() # forces it to actually parse SQ -class TestUnknownVR(object): +class TestUnknownVR: @pytest.mark.parametrize( - 'vr_bytes, str_output', + "vr_bytes, str_output", [ # Test limits of char values - (b'\x00\x41', '0x00 0x41'), # 000/A - (b'\x40\x41', '0x40 0x41'), # 064/A - (b'\x5B\x41', '0x5b 0x41'), # 091/A - (b'\x60\x41', '0x60 0x41'), # 096/A - (b'\x7B\x41', '0x7b 0x41'), # 123/A - (b'\xFF\x41', '0xff 0x41'), # 255/A + (b"\x00\x41", "0x00 0x41"), # 000/A + (b"\x40\x41", "0x40 0x41"), # 064/A + (b"\x5B\x41", "0x5b 0x41"), # 091/A + (b"\x60\x41", "0x60 0x41"), # 096/A + (b"\x7B\x41", "0x7b 0x41"), # 123/A + (b"\xFF\x41", "0xff 0x41"), # 255/A # Test good/bad - (b'\x41\x00', '0x41 0x00'), # A/- - (b'\x5A\x00', '0x5a 0x00'), # Z/- + (b"\x41\x00", "0x41 0x00"), # A/- + (b"\x5A\x00", "0x5a 0x00"), # Z/- # Test not quite good/bad - (b'\x61\x00', '0x61 0x00'), # a/- - (b'\x7A\x00', '0x7a 0x00'), # z/- + (b"\x61\x00", "0x61 0x00"), # a/- + (b"\x7A\x00", "0x7a 0x00"), # z/- # Test bad/good - (b'\x00\x41', '0x00 0x41'), # -/A - (b'\x00\x5A', '0x00 0x5a'), # -/Z + (b"\x00\x41", "0x00 0x41"), # -/A + (b"\x00\x5A", "0x00 0x5a"), # -/Z # Test bad/not quite good - (b'\x00\x61', '0x00 0x61'), # -/a - (b'\x00\x7A', '0x00 0x7a'), # -/z + (b"\x00\x61", "0x00 0x61"), # -/a + (b"\x00\x7A", "0x00 0x7a"), # -/z # Test good/good - (b'\x41\x41', 'AA'), # A/A - (b'\x41\x5A', 'AZ'), # A/Z - (b'\x5A\x41', 'ZA'), # Z/A - (b'\x5A\x5A', 'ZZ'), # Z/Z + (b"\x41\x41", "AA"), # A/A + (b"\x41\x5A", "AZ"), # A/Z + (b"\x5A\x41", "ZA"), # Z/A + (b"\x5A\x5A", "ZZ"), # Z/Z # Test not quite good - (b'\x41\x61', 'Aa'), # A/a - (b'\x41\x7A', 'Az'), # A/z - (b'\x61\x41', 'aA'), # a/A - (b'\x61\x5A', 'aZ'), # a/Z - (b'\x61\x61', 'aa'), # a/a - (b'\x61\x7A', 'az'), # a/z - (b'\x5A\x61', 'Za'), # Z/a - (b'\x5A\x7A', 'Zz'), # Z/z - (b'\x7A\x41', 'zA'), # z/A - (b'\x7A\x5A', 'zZ'), # z/Z - (b'\x7A\x61', 'za'), # z/a - (b'\x7A\x7A', 'zz'), # z/z - ] + (b"\x41\x61", "Aa"), # A/a + (b"\x41\x7A", "Az"), # A/z + (b"\x61\x41", "aA"), # a/A + (b"\x61\x5A", "aZ"), # a/Z + (b"\x61\x61", "aa"), # a/a + (b"\x61\x7A", "az"), # a/z + (b"\x5A\x61", "Za"), # Z/a + (b"\x5A\x7A", "Zz"), # Z/z + (b"\x7A\x41", "zA"), # z/A + (b"\x7A\x5A", "zZ"), # z/Z + (b"\x7A\x61", "za"), # z/a + (b"\x7A\x7A", "zz"), # z/z + ], ) def test_fail_decode_msg(self, vr_bytes, str_output): """Regression test for #791.""" @@ -810,59 +926,80 @@ # as the first tag is used to check the VR ds = read_dataset( BytesIO( - b'\x08\x00\x05\x00CS\x0a\x00ISO_IR 100' - b'\x08\x00\x06\x00' + - vr_bytes + - b'\x00\x00\x00\x08\x00\x49' + b"\x08\x00\x05\x00CS\x0a\x00ISO_IR 100" + b"\x08\x00\x06\x00" + vr_bytes + b"\x00\x00\x00\x08\x00\x49" ), - False, True - ) - msg = ( - r"Unknown Value Representation '{}' in tag \(0008, 0006\)" - .format(str_output) + False, + True, ) + msg = r"Unknown Value Representation '{}' in tag \(0008, 0006\)" + msg = msg.format(str_output) with pytest.raises(NotImplementedError, match=msg): print(ds) -class TestReadDataElement(object): +class TestReadDataElement: def setup(self): ds = Dataset() - ds.DoubleFloatPixelData = (b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03\x04\x05\x06\x07') # OD - ds.SelectorOLValue = (b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03') # VR of OL - ds.PotentialReasonsForProcedure = ['A', 'B', - 'C'] # VR of UC, odd length - ds.StrainDescription = 'Test' # Even length - ds.URNCodeValue = 'http://test.com' # VR of UR - ds.RetrieveURL = 'ftp://test.com ' # Test trailing spaces ignored - ds.DestinationAE = ' TEST 12 ' # 16 characters max for AE + ds.DoubleFloatPixelData = ( + b"\x00\x01\x02\x03\x04\x05\x06\x07" + b"\x01\x01\x02\x03\x04\x05\x06\x07" + ) # OD + ds.SelectorOLValue = ( + b"\x00\x01\x02\x03\x04\x05\x06\x07" b"\x01\x01\x02\x03" + ) # VR of OL + ds.PotentialReasonsForProcedure = [ + "A", + "B", + "C", + ] # VR of UC, odd length + ds.StrainDescription = "Test" # Even length + ds.URNCodeValue = "http://test.com" # VR of UR + ds.RetrieveURL = "ftp://test.com " # Test trailing spaces ignored + ds.DestinationAE = " TEST 12 " # 16 characters max for AE # 8-byte values ds.ExtendedOffsetTable = ( # VR of OV - b'\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x01\x02\x03\x04\x05\x06\x07\x08' + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x02\x03\x04\x05\x06\x07\x08" ) # No public elements with VR of SV or UV yet... - add_dict_entries({ - 0xFFFE0001: ( - 'SV', '1', 'SV Element Minimum', '', 'SVElementMinimum' - ), - 0xFFFE0002: ( - 'SV', '1', 'SV Element Maximum', '', 'SVElementMaximum' - ), - 0xFFFE0003: ( - 'UV', '1', 'UV Element Minimum', '', 'UVElementMinimum' - ), - 0xFFFE0004: ( - 'UV', '1', 'UV Element Maximum', '', 'UVElementMaximum' - ), - }) - ds.SVElementMinimum = -2**63 - ds.SVElementMaximum = 2**63 - 1 + add_dict_entries( + { + 0xFFFE0001: ( + "SV", + "1", + "SV Element Minimum", + "", + "SVElementMinimum", + ), + 0xFFFE0002: ( + "SV", + "1", + "SV Element Maximum", + "", + "SVElementMaximum", + ), + 0xFFFE0003: ( + "UV", + "1", + "UV Element Minimum", + "", + "UVElementMinimum", + ), + 0xFFFE0004: ( + "UV", + "1", + "UV Element Maximum", + "", + "UVElementMaximum", + ), + } + ) + ds.SVElementMinimum = -(2 ** 63) + ds.SVElementMaximum = 2 ** 63 - 1 ds.UVElementMinimum = 0 - ds.UVElementMaximum = 2**64 - 1 + ds.UVElementMaximum = 2 ** 64 - 1 self.fp = BytesIO() # Implicit little file_ds = FileDataset(self.fp, ds) @@ -879,204 +1016,317 @@ def test_read_OD_implicit_little(self): """Check creation of OD DataElement from byte data works correctly.""" ds = dcmread(self.fp, force=True) - ref_elem = ds.get(0x7fe00009) - elem = DataElement(0x7fe00009, 'OD', - b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03\x04\x05\x06\x07') + ref_elem = ds.get(0x7FE00009) + elem = DataElement( + 0x7FE00009, + "OD", + b"\x00\x01\x02\x03\x04\x05\x06\x07" + b"\x01\x01\x02\x03\x04\x05\x06\x07", + ) assert ref_elem == elem def test_read_OD_explicit_little(self): """Check creation of OD DataElement from byte data works correctly.""" ds = dcmread(self.fp_ex, force=True) - ref_elem = ds.get(0x7fe00009) - elem = DataElement(0x7fe00009, 'OD', - b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03\x04\x05\x06\x07') + ref_elem = ds.get(0x7FE00009) + elem = DataElement( + 0x7FE00009, + "OD", + b"\x00\x01\x02\x03\x04\x05\x06\x07" + b"\x01\x01\x02\x03\x04\x05\x06\x07", + ) assert ref_elem == elem def test_read_OL_implicit_little(self): """Check creation of OL DataElement from byte data works correctly.""" ds = dcmread(self.fp, force=True) ref_elem = ds.get(0x00720075) - elem = DataElement(0x00720075, 'OL', - b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03') + elem = DataElement( + 0x00720075, + "OL", + b"\x00\x01\x02\x03\x04\x05\x06\x07" b"\x01\x01\x02\x03", + ) assert ref_elem == elem def test_read_OL_explicit_little(self): """Check creation of OL DataElement from byte data works correctly.""" ds = dcmread(self.fp_ex, force=True) ref_elem = ds.get(0x00720075) - elem = DataElement(0x00720075, 'OL', - b'\x00\x01\x02\x03\x04\x05\x06\x07' - b'\x01\x01\x02\x03') + elem = DataElement( + 0x00720075, + "OL", + b"\x00\x01\x02\x03\x04\x05\x06\x07" b"\x01\x01\x02\x03", + ) assert ref_elem == elem def test_read_UC_implicit_little(self): """Check creation of DataElement from byte data works correctly.""" ds = dcmread(self.fp, force=True) ref_elem = ds.get(0x00189908) - elem = DataElement(0x00189908, 'UC', ['A', 'B', 'C']) + elem = DataElement(0x00189908, "UC", ["A", "B", "C"]) assert ref_elem == elem ds = dcmread(self.fp, force=True) ref_elem = ds.get(0x00100212) - elem = DataElement(0x00100212, 'UC', 'Test') + elem = DataElement(0x00100212, "UC", "Test") assert ref_elem == elem def test_read_UC_explicit_little(self): """Check creation of DataElement from byte data works correctly.""" ds = dcmread(self.fp_ex, force=True) ref_elem = ds.get(0x00189908) - elem = DataElement(0x00189908, 'UC', ['A', 'B', 'C']) + elem = DataElement(0x00189908, "UC", ["A", "B", "C"]) assert ref_elem == elem ds = dcmread(self.fp_ex, force=True) ref_elem = ds.get(0x00100212) - elem = DataElement(0x00100212, 'UC', 'Test') + elem = DataElement(0x00100212, "UC", "Test") assert ref_elem == elem def test_read_UR_implicit_little(self): """Check creation of DataElement from byte data works correctly.""" ds = dcmread(self.fp, force=True) ref_elem = ds.get(0x00080120) # URNCodeValue - elem = DataElement(0x00080120, 'UR', 'http://test.com') + elem = DataElement(0x00080120, "UR", "http://test.com") assert ref_elem == elem # Test trailing spaces ignored ref_elem = ds.get(0x00081190) # RetrieveURL - elem = DataElement(0x00081190, 'UR', 'ftp://test.com') + elem = DataElement(0x00081190, "UR", "ftp://test.com") assert ref_elem == elem def test_read_UR_explicit_little(self): """Check creation of DataElement from byte data works correctly.""" ds = dcmread(self.fp_ex, force=True) ref_elem = ds.get(0x00080120) # URNCodeValue - elem = DataElement(0x00080120, 'UR', 'http://test.com') + elem = DataElement(0x00080120, "UR", "http://test.com") assert ref_elem == elem # Test trailing spaces ignored ref_elem = ds.get(0x00081190) # RetrieveURL - elem = DataElement(0x00081190, 'UR', 'ftp://test.com') + elem = DataElement(0x00081190, "UR", "ftp://test.com") assert ref_elem == elem def test_read_AE(self): """Check creation of AE DataElement from byte data works correctly.""" ds = dcmread(self.fp, force=True) - assert 'TEST 12' == ds.DestinationAE + assert "TEST 12" == ds.DestinationAE def test_read_OV_implicit_little(self): """Check reading element with VR of OV encoded as implicit""" ds = dcmread(self.fp, force=True) val = ( - b'\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x01\x02\x03\x04\x05\x06\x07\x08' + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x02\x03\x04\x05\x06\x07\x08" ) - elem = ds['ExtendedOffsetTable'] - assert 'OV' == elem.VR + elem = ds["ExtendedOffsetTable"] + assert "OV" == elem.VR assert 0x7FE00001 == elem.tag assert val == elem.value - new = DataElement(0x7FE00001, 'OV', val) + new = DataElement(0x7FE00001, "OV", val) assert elem == new def test_read_OV_explicit_little(self): """Check reading element with VR of OV encoded as explicit""" ds = dcmread(self.fp_ex, force=True) val = ( - b'\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x01\x02\x03\x04\x05\x06\x07\x08' + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x02\x03\x04\x05\x06\x07\x08" ) - elem = ds['ExtendedOffsetTable'] - assert 'OV' == elem.VR + elem = ds["ExtendedOffsetTable"] + assert "OV" == elem.VR assert 0x7FE00001 == elem.tag assert val == elem.value - new = DataElement(0x7FE00001, 'OV', val) + new = DataElement(0x7FE00001, "OV", val) assert elem == new def test_read_SV_implicit_little(self): """Check reading element with VR of SV encoded as implicit""" ds = dcmread(self.fp, force=True) - elem = ds['SVElementMinimum'] - assert 'SV' == elem.VR + elem = ds["SVElementMinimum"] + assert "SV" == elem.VR assert 0xFFFE0001 == elem.tag - assert -2**63 == elem.value + assert -(2 ** 63) == elem.value - new = DataElement(0xFFFE0001, 'SV', -2**63) + new = DataElement(0xFFFE0001, "SV", -(2 ** 63)) assert elem == new - elem = ds['SVElementMaximum'] - assert 'SV' == elem.VR + elem = ds["SVElementMaximum"] + assert "SV" == elem.VR assert 0xFFFE0002 == elem.tag - assert 2**63 - 1 == elem.value + assert 2 ** 63 - 1 == elem.value - new = DataElement(0xFFFE0002, 'SV', 2**63 - 1) + new = DataElement(0xFFFE0002, "SV", 2 ** 63 - 1) assert elem == new @pytest.mark.skip("No public elements with VR of SV") def test_read_SV_explicit_little(self): """Check reading element with VR of SV encoded as explicit""" ds = dcmread(self.fp_ex, force=True) - elem = ds['SVElementMinimum'] - assert 'SV' == elem.VR + elem = ds["SVElementMinimum"] + assert "SV" == elem.VR assert 0xFFFE0001 == elem.tag - assert -2**63 == elem.value + assert -(2 ** 63) == elem.value - new = DataElement(0xFFFE0001, 'SV', -2**63) + new = DataElement(0xFFFE0001, "SV", -(2 ** 63)) assert elem == new - elem = ds['SVElementMaximum'] - assert 'SV' == elem.VR + elem = ds["SVElementMaximum"] + assert "SV" == elem.VR assert 0xFFFE0002 == elem.tag - assert 2**63 - 1 == elem.value + assert 2 ** 63 - 1 == elem.value - new = DataElement(0xFFFE0002, 'SV', 2**63 - 1) + new = DataElement(0xFFFE0002, "SV", 2 ** 63 - 1) assert elem == new def test_read_UV_implicit_little(self): """Check reading element with VR of UV encoded as implicit""" ds = dcmread(self.fp, force=True) - elem = ds['UVElementMinimum'] - assert 'UV' == elem.VR + elem = ds["UVElementMinimum"] + assert "UV" == elem.VR assert 0xFFFE0003 == elem.tag assert 0 == elem.value - new = DataElement(0xFFFE0003, 'UV', 0) + new = DataElement(0xFFFE0003, "UV", 0) assert elem == new - elem = ds['UVElementMaximum'] - assert 'UV' == elem.VR + elem = ds["UVElementMaximum"] + assert "UV" == elem.VR assert 0xFFFE0004 == elem.tag - assert 2**64 - 1 == elem.value + assert 2 ** 64 - 1 == elem.value - new = DataElement(0xFFFE0004, 'UV', 2**64 - 1) + new = DataElement(0xFFFE0004, "UV", 2 ** 64 - 1) assert elem == new def test_read_UV_explicit_little(self): """Check reading element with VR of UV encoded as explicit""" ds = dcmread(self.fp_ex, force=True) - elem = ds['UVElementMinimum'] - assert 'UV' == elem.VR + elem = ds["UVElementMinimum"] + assert "UV" == elem.VR assert 0xFFFE0003 == elem.tag assert 0 == elem.value - new = DataElement(0xFFFE0003, 'UV', 0) + new = DataElement(0xFFFE0003, "UV", 0) assert elem == new - elem = ds['UVElementMaximum'] - assert 'UV' == elem.VR + elem = ds["UVElementMaximum"] + assert "UV" == elem.VR assert 0xFFFE0004 == elem.tag - assert 2**64 - 1 == elem.value + assert 2 ** 64 - 1 == elem.value - new = DataElement(0xFFFE0004, 'UV', 2**64 - 1) + new = DataElement(0xFFFE0004, "UV", 2 ** 64 - 1) assert elem == new -class TestDeferredRead(object): +class TestDSISnumpy: + def setup(self): + self.orig_IS_numpy = config.use_IS_numpy + self.orig_DS_numpy = config.use_DS_numpy + self.orig_DS_decimal = config.use_DS_decimal + + def teardown(self): + config.use_IS_numpy = self.orig_IS_numpy + config.DS_decimal(self.orig_DS_decimal) + config.DS_numpy(self.orig_DS_numpy) + + @pytest.mark.skipif(have_numpy, reason="Testing import error") + def test_IS_numpy_import_error(self): + config.use_IS_numpy = True + rtss = dcmread(rtstruct_name, force=True) + # no numpy, then trying to use numpy raises error + with pytest.raises(ImportError): + col = rtss.ROIContourSequence[0].ROIDisplayColor # VR is IS + + @pytest.mark.skipif(not have_numpy, reason="Testing with numpy only") + def test_IS_numpy_class(self): + config.use_IS_numpy = True + rtss = dcmread(rtstruct_name, force=True) + col = rtss.ROIContourSequence[0].ROIDisplayColor # VR is IS + assert isinstance(col, numpy.ndarray) + assert "int64" == col.dtype + + # Check a conversion with only a single value + roi_num = rtss.ROIContourSequence[0].ReferencedROINumber + assert isinstance(roi_num, numpy.int64) + + def test_IS_not_numpy(self): + """Test class of the object matches the config, + when the config is changed""" + config.use_IS_numpy = False + rtss = dcmread(rtstruct_name, force=True) + col = rtss.ROIContourSequence[0].ROIDisplayColor # VR is IS + assert isinstance(col, MultiValue) + + @pytest.mark.skipif(have_numpy, reason="Testing import error") + def test_DS_numpy_import_error(self): + config.use_DS_numpy = True + rtss = dcmread(rtstruct_name, force=True) + # no numpy, then trying to use numpy raises error + with pytest.raises(ImportError): + cd = rtss.ROIContourSequence[0].ContourSequence[0].ContourData + + @pytest.mark.skipif(not have_numpy, reason="Testing with numpy only") + def test_DS_numpy_class(self): + config.use_DS_numpy = True + rtss = dcmread(rtstruct_name, force=True) + # ContourData has VR of DS + cd = rtss.ROIContourSequence[0].ContourSequence[0].ContourData + assert isinstance(cd, numpy.ndarray) + assert "float64" == cd.dtype + + # Check conversion with only a single value + roi_vol = rtss.StructureSetROISequence[0].ROIVolume + assert isinstance(roi_vol, numpy.float64) + + def test_DS_not_numpy(self): + """Test class of the object matches the config.""" + config.use_DS_numpy = False + rtss = dcmread(rtstruct_name, force=True) + # ContourData has VR of DS + cd = rtss.ROIContourSequence[0].ContourSequence[0].ContourData + assert isinstance(cd, MultiValue) + + @pytest.mark.skipif(not have_numpy, reason="numpy not installed") + def test_DS_conflict_config(self): + config.DS_decimal(True) + with pytest.raises(ValueError): + config.DS_numpy(True) + + @pytest.mark.skipif(not have_numpy, reason="numpy not installed") + def test_DS_conflict_config2(self): + config.DS_numpy(True) + with pytest.raises(ValueError): + config.DS_decimal(True) + + @pytest.mark.skipif(not have_numpy, reason="numpy not installed") + def test_DS_bad_chars(self): + config.DS_numpy(True) + with pytest.raises(ValueError): + values.convert_DS_string(b"123.1b", True) + + @pytest.mark.skipif(not have_numpy, reason="numpy not installed") + def test_IS_bad_chars(self): + config.use_IS_numpy = True + with pytest.raises(ValueError): + values.convert_IS_string(b"123b", True) + + @pytest.mark.skipif(have_numpy, reason="testing numpy ImportError") + def test_numpy_import_warning(self): + config.DS_numpy(True) + config.use_IS_numpy = True + with pytest.raises(ImportError): + values.convert_DS_string(b"123.1", True) + with pytest.raises(ImportError): + values.convert_IS_string(b"123", True) + + +class TestDeferredRead: """Test that deferred data element reading (for large size) works as expected """ + # Copy one of test files and use temporarily, then later remove. def setup(self): self.testfile_name = ct_name + ".tmp" @@ -1088,11 +1338,12 @@ def test_time_check(self): """Deferred read warns if file has been modified""" - ds = dcmread(self.testfile_name, defer_size='2 kB') + ds = dcmread(self.testfile_name, defer_size="2 kB") from time import sleep + sleep(0.1) with open(self.testfile_name, "r+") as f: - f.write('\0') # "touch" the file + f.write("\0") # "touch" the file msg = r"Deferred read warning -- file modification time has changed" with pytest.warns(UserWarning, match=msg): @@ -1111,7 +1362,11 @@ ds_defer = dcmread(self.testfile_name, defer_size=2000) for data_elem in ds_norm: tag = data_elem.tag - assert data_elem.value == ds_defer[tag].value + + if have_numpy and isinstance(data_elem.value, numpy.ndarray): + assert numpy.allclose(data_elem.value, ds_defer[tag].value) + else: + assert data_elem.value == ds_defer[tag].value def test_zipped_deferred(self): """Deferred values from a gzipped file works.""" @@ -1125,24 +1380,31 @@ def test_filelike_deferred(self): """Deferred values work with file-like objects.""" - with open(ct_name, 'rb') as fp: + with open(ct_name, "rb") as fp: data = fp.read() filelike = io.BytesIO(data) dataset = pydicom.dcmread(filelike, defer_size=1024) assert 32768 == len(dataset.PixelData) -class TestReadTruncatedFile(object): +class TestReadTruncatedFile: def testReadFileWithMissingPixelData(self): mr = dcmread(truncated_mr_name) mr.decode() - assert 'CompressedSamples^MR1' == mr.PatientName + assert "CompressedSamples^MR1" == mr.PatientName assert mr.PatientName == mr[0x10, 0x10].value DS = pydicom.valuerep.DS - assert [DS('0.3125'), DS('0.3125')] == mr.PixelSpacing - @pytest.mark.skipif(not have_numpy or have_gdcm_handler, - reason="Missing numpy or GDCM present") + if have_numpy and config.use_DS_numpy: + expected = numpy.array([0.3125, 0.3125]) + assert numpy.allclose(mr.PixelSpacing, expected) + else: + assert [DS("0.3125"), DS("0.3125")] == mr.PixelSpacing + + @pytest.mark.skipif( + not have_numpy or have_gdcm_handler, + reason="Missing numpy or GDCM present", + ) def testReadFileWithMissingPixelDataArray(self): mr = dcmread(truncated_mr_name) mr.decode() @@ -1157,29 +1419,31 @@ mr.pixel_array -class TestFileLike(object): +class TestFileLike: """Test that can read DICOM files with file-like object rather than filename """ + def test_read_file_given_file_object(self): """filereader: can read using already opened file............""" - f = open(ct_name, 'rb') + f = open(ct_name, "rb") ct = dcmread(f) - # Tests here simply repeat testCT -- perhaps should collapse + # XXX Tests here simply repeat testCT -- perhaps should collapse # the code together? - got = ct.ImagePositionPatient DS = pydicom.valuerep.DS - expected = [DS('-158.135803'), DS('-179.035797'), DS('-75.699997')] - assert expected == got - assert '1.3.6.1.4.1.5962.2' == ct.file_meta.ImplementationClassUID + + got = ct.ImagePositionPatient + if have_numpy and config.use_DS_numpy: + expected = numpy.array([-158.135803, -179.035797, -75.699997]) + assert numpy.allclose(got, expected) + else: + expected = [DS("-158.135803"), DS("-179.035797"), DS("-75.699997")] + assert expected == got + + assert "1.3.6.1.4.1.5962.2" == ct.file_meta.ImplementationClassUID value = ct.file_meta[0x2, 0x12].value assert ct.file_meta.ImplementationClassUID == value - # (0020, 0032) Image Position (Patient) - # [-158.13580300000001, -179.035797, -75.699996999999996] - got = ct.ImagePositionPatient - expected = [DS('-158.135803'), DS('-179.035797'), DS('-75.699997')] - assert expected == got assert 128 == ct.Rows assert 128 == ct.Columns assert 16 == ct.BitsStored @@ -1191,56 +1455,57 @@ def test_read_file_given_file_like_object(self): """filereader: can read using a file-like (BytesIO) file....""" - with open(ct_name, 'rb') as f: + with open(ct_name, "rb") as f: file_like = BytesIO(f.read()) ct = dcmread(file_like) # Tests here simply repeat some of testCT test got = ct.ImagePositionPatient DS = pydicom.valuerep.DS - expected = [DS('-158.135803'), DS('-179.035797'), DS('-75.699997')] - assert expected == got + + if have_numpy and config.use_DS_numpy: + expected = numpy.array([-158.135803, -179.035797, -75.699997]) + assert numpy.allclose(got, expected) + else: + expected = [DS("-158.135803"), DS("-179.035797"), DS("-75.699997")] + assert expected == got + assert 128 * 128 * 2 == len(ct.PixelData) + # Should also be able to close the file ourselves without # exception raised: file_like.close() -class TestDataElementGenerator(object): +class TestDataElementGenerator: """Test filereader.data_element_generator""" + def test_little_endian_explicit(self): """Test reading little endian explicit VR data""" # (0010, 0010) PatientName PN 6 ABCDEF - bytestream = (b'\x10\x00\x10\x00' - b'PN' - b'\x06\x00' - b'ABCDEF') + bytestream = b"\x10\x00\x10\x00" b"PN" b"\x06\x00" b"ABCDEF" fp = BytesIO(bytestream) # fp, is_implicit_VR, is_little_endian, gen = data_element_generator(fp, False, True) - elem = DataElement(0x00100010, 'PN', 'ABCDEF') - assert elem == DataElement_from_raw(next(gen), 'ISO_IR 100') + elem = DataElement(0x00100010, "PN", "ABCDEF") + assert elem == DataElement_from_raw(next(gen), "ISO_IR 100") def test_little_endian_implicit(self): """Test reading little endian implicit VR data""" # (0010, 0010) PatientName PN 6 ABCDEF - bytestream = b'\x10\x00\x10\x00' \ - b'\x06\x00\x00\x00' \ - b'ABCDEF' + bytestream = b"\x10\x00\x10\x00" b"\x06\x00\x00\x00" b"ABCDEF" fp = BytesIO(bytestream) - gen = data_element_generator(fp, is_implicit_VR=True, - is_little_endian=True) - elem = DataElement(0x00100010, 'PN', 'ABCDEF') - assert elem == DataElement_from_raw(next(gen), 'ISO_IR 100') + gen = data_element_generator( + fp, is_implicit_VR=True, is_little_endian=True + ) + elem = DataElement(0x00100010, "PN", "ABCDEF") + assert elem == DataElement_from_raw(next(gen), "ISO_IR 100") def test_big_endian_explicit(self): """Test reading big endian explicit VR data""" # (0010, 0010) PatientName PN 6 ABCDEF - bytestream = b'\x00\x10\x00\x10' \ - b'PN' \ - b'\x00\x06' \ - b'ABCDEF' + bytestream = b"\x00\x10\x00\x10" b"PN" b"\x00\x06" b"ABCDEF" fp = BytesIO(bytestream) # fp, is_implicit_VR, is_little_endian, gen = data_element_generator(fp, False, False) - elem = DataElement(0x00100010, 'PN', 'ABCDEF') - assert elem == DataElement_from_raw(next(gen), 'ISO_IR 100') + elem = DataElement(0x00100010, "PN", "ABCDEF") + assert elem == DataElement_from_raw(next(gen), "ISO_IR 100") diff -Nru pydicom-1.4.1/pydicom/tests/test_fileutil.py pydicom-2.0.0/pydicom/tests/test_fileutil.py --- pydicom-1.4.1/pydicom/tests/test_fileutil.py 1970-01-01 00:00:00.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_fileutil.py 2020-05-29 01:44:31.000000000 +0000 @@ -0,0 +1,37 @@ +# Copyright 2008-2020 pydicom authors. See LICENSE file for details. +"""Test suite for util functions""" +import sys +from io import BytesIO +from pathlib import Path + +import pytest + +from pydicom.fileutil import path_from_pathlike + + +class PathLike: + """Minimal example for path-like object""" + def __init__(self, path: str): + self.path = path + + def __fspath__(self): + return self.path + + +class TestPathFromPathLike: + """Test the fileutil module""" + + def test_non_pathlike_is_returned_unaltered(self): + assert 'test.dcm' == path_from_pathlike('test.dcm') + assert path_from_pathlike(None) is None + file_like = BytesIO() + assert file_like == path_from_pathlike(file_like) + assert 42 == path_from_pathlike(42) + + def test_pathlib_path(self): + assert 'test.dcm' == path_from_pathlike(Path('test.dcm')) + + @pytest.mark.skipif(sys.version_info < (3, 6), + reason="Path-like objects introduced in Python 3.6") + def test_path_like(self): + assert 'test.dcm' == path_from_pathlike(PathLike('test.dcm')) diff -Nru pydicom-1.4.1/pydicom/tests/test_filewriter.py pydicom-2.0.0/pydicom/tests/test_filewriter.py --- pydicom-1.4.1/pydicom/tests/test_filewriter.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_filewriter.py 2020-05-29 01:44:31.000000000 +0000 @@ -3,33 +3,35 @@ """test cases for pydicom.filewriter module""" import tempfile from copy import deepcopy -from datetime import date, datetime, time, timedelta +from datetime import date, datetime, time, timedelta, timezone from io import BytesIO import os +from pathlib import Path from platform import python_implementation from struct import unpack from tempfile import TemporaryFile +import zlib import pytest from pydicom._storage_sopclass_uids import CTImageStorage from pydicom import config, __version_info__, uid from pydicom.data import get_testdata_file, get_charset_files -from pydicom.dataset import Dataset, FileDataset +from pydicom.dataset import Dataset, FileDataset, FileMetaDataset from pydicom.dataelem import DataElement, RawDataElement from pydicom.filebase import DicomBytesIO from pydicom.filereader import dcmread, read_dataset, read_file -from pydicom.filewriter import (write_data_element, write_dataset, - correct_ambiguous_vr, write_file_meta_info, - correct_ambiguous_vr_element, write_numbers, - write_PN, _format_DT, write_text) +from pydicom.filewriter import ( + write_data_element, write_dataset, correct_ambiguous_vr, + write_file_meta_info, correct_ambiguous_vr_element, write_numbers, + write_PN, _format_DT, write_text, write_OWvalue +) from pydicom.multival import MultiValue from pydicom.sequence import Sequence from pydicom.uid import (ImplicitVRLittleEndian, ExplicitVRBigEndian, PYDICOM_IMPLEMENTATION_UID) from pydicom.util.hexutil import hex2bytes -from pydicom.util.fixes import timezone from pydicom.valuerep import DA, DT, TM from pydicom.values import convert_text from ._write_stds import impl_LE_deflen_std_hex @@ -48,6 +50,7 @@ unicode_name = get_charset_files("chrH31.dcm")[0] multiPN_name = get_charset_files("chrFrenMulti.dcm")[0] +deflate_name = get_testdata_file("image_dfl.dcm") base_version = '.'.join(str(i) for i in __version_info__) @@ -76,7 +79,19 @@ return False, pos # False if not identical, position of 1st diff -class TestWriteFile(object): +def as_assertable(dataset): + """Copy the elements in a Dataset (including the file_meta, if any) + to a set that can be safely compared using pytest's assert. + (Datasets can't be so compared because DataElements are not + hashable.)""" + safe_dict = dict((str(elem.tag) + " " + elem.keyword, elem.value) + for elem in dataset) + if hasattr(dataset, "file_meta"): + safe_dict.update(as_assertable(dataset.file_meta)) + return safe_dict + + +class TestWriteFile: def setup(self): self.file_out = TemporaryFile('w+b') @@ -138,6 +153,14 @@ them identical (JPEG2K file).""" self.compare(jpeg_name) + def test_pathlib_path_filename(self): + """Check that file can be written using pathlib.Path""" + ds = dcmread(Path(ct_name)) + ds.save_as(self.file_out, write_like_original=True) + self.file_out.seek(0) + ds1 = dcmread(self.file_out) + assert ds.PatientName == ds1.PatientName + def testListItemWriteBack(self): """Change item in a list and confirm it is written to file""" @@ -185,7 +208,7 @@ """Test writing element (FFFF, FFFF) to file #92""" fp = DicomBytesIO() ds = Dataset() - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.is_little_endian = True ds.is_implicit_VR = True ds.add_new(0xFFFFFFFF, 'LO', '123456') @@ -213,6 +236,41 @@ ds = read_file(self.file_out) assert ds.PerformedProcedureCodeSequence == [] + def test_write_deflated_retains_elements(self): + """Read a Deflated Explicit VR Little Endian file, write it, + and then read the output, to verify that the written file + contains the same data. + """ + original = read_file(deflate_name) + original.save_as(self.file_out) + + self.file_out.seek(0) + rewritten = read_file(self.file_out) + + assert as_assertable(rewritten) == as_assertable(original) + + def test_write_deflated_deflates_post_file_meta(self): + """Read a Deflated Explicit VR Little Endian file, write it, + and then check the bytes in the output, to verify that the + written file is deflated past the file meta information. + """ + original = read_file(deflate_name) + original.save_as(self.file_out) + + first_byte_past_file_meta = 0x14e + with open(deflate_name, "rb") as original_file: + original_file.seek(first_byte_past_file_meta) + original_post_meta_file_bytes = original_file.read() + unzipped_original = zlib.decompress(original_post_meta_file_bytes, + -zlib.MAX_WBITS) + + self.file_out.seek(first_byte_past_file_meta) + rewritten_post_meta_file_bytes = self.file_out.read() + unzipped_rewritten = zlib.decompress(rewritten_post_meta_file_bytes, + -zlib.MAX_WBITS) + + assert unzipped_rewritten == unzipped_original + class TestScratchWriteDateTime(TestWriteFile): """Write and reread simple or multi-value DA/DT/TM data elements""" @@ -256,7 +314,7 @@ assert TM_expected == ds.TimeOfLastCalibration -class TestWriteDataElement(object): +class TestWriteDataElement: """Attempt to write data elements has the expected behaviour""" def setup(self): @@ -659,7 +717,7 @@ write_data_element(fp, elem) -class TestCorrectAmbiguousVR(object): +class TestCorrectAmbiguousVR: """Test correct_ambiguous_vr.""" def test_pixel_representation_vm_one(self): @@ -969,7 +1027,7 @@ assert 'SS' == ds[0x00283000][0][0x00283002].VR -class TestCorrectAmbiguousVRElement(object): +class TestCorrectAmbiguousVRElement: """Test filewriter.correct_ambiguous_vr_element""" def test_not_ambiguous(self): @@ -1022,7 +1080,7 @@ assert out.value == 0xfffe -class TestWriteAmbiguousVR(object): +class TestWriteAmbiguousVR: """Attempt to write data elements with ambiguous VR.""" def setup(self): @@ -1082,7 +1140,7 @@ assert ['UTF8'] == ds.read_encoding -class TestScratchWrite(object): +class TestScratchWrite: """Simple dataset from scratch, written in all endian/VR combinations""" def setup(self): @@ -1134,7 +1192,7 @@ self.compare_write(impl_LE_deflen_std_hex, file_ds) -class TestWriteToStandard(object): +class TestWriteToStandard: """Unit tests for writing datasets to the DICOM standard""" def test_preamble_default(self): @@ -1490,7 +1548,7 @@ version = 'PYDICOM ' + base_version ds = dcmread(rtplan_name) transfer_syntax = ds.file_meta.TransferSyntaxUID - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.save_as(fp, write_like_original=False) fp.seek(0) out = dcmread(fp) @@ -1517,7 +1575,7 @@ """Test exception is raised if trying to write with no file_meta.""" ds = dcmread(rtplan_name) del ds.SOPInstanceUID - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() with pytest.raises(ValueError): ds.save_as(DicomBytesIO(), write_like_original=False) del ds.file_meta @@ -1571,7 +1629,7 @@ assert 'MessageID' not in ds_out -class TestWriteFileMetaInfoToStandard(object): +class TestWriteFileMetaInfoToStandard: """Unit tests for writing File Meta Info to the DICOM standard.""" def test_bad_elements(self): @@ -1731,7 +1789,7 @@ assert test_length == 68 + class_length + version_length -class TestWriteNonStandard(object): +class TestWriteNonStandard: """Unit tests for writing datasets not to the DICOM standard.""" def setup(self): @@ -1794,7 +1852,7 @@ def test_file_meta_unchanged(self): """Test no file_meta elements are added if missing.""" ds = dcmread(rtplan_name) - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.save_as(self.fp, write_like_original=True) assert Dataset() == ds.file_meta @@ -2071,7 +2129,7 @@ self.compare_bytes(bytes_in.getvalue(), bytes_out.getvalue()) -class TestWriteFileMetaInfoNonStandard(object): +class TestWriteFileMetaInfoNonStandard: """Unit tests for writing File Meta Info not to the DICOM standard.""" def setup(self): @@ -2193,7 +2251,7 @@ assert ref_meta == meta -class TestWriteNumbers(object): +class TestWriteNumbers: """Test filewriter.write_numbers""" def test_write_empty_value(self): @@ -2243,7 +2301,33 @@ assert fp.getvalue() == b'\x00\x01' -class TestWritePN(object): +class TestWriteOtherVRs: + """Tests for writing the 'O' VRs like OB, OW, OF, etc.""" + def test_write_of(self): + """Test writing element with VR OF""" + fp = DicomBytesIO() + fp.is_little_endian = True + elem = DataElement(0x7fe00008, 'OF', b'\x00\x01\x02\x03') + write_OWvalue(fp, elem) + assert fp.getvalue() == b'\x00\x01\x02\x03' + + def test_write_of_dataset(self): + """Test writing a dataset with an element with VR OF.""" + fp = DicomBytesIO() + fp.is_little_endian = True + fp.is_implicit_VR = False + ds = Dataset() + ds.is_little_endian = True + ds.is_implicit_VR = False + ds.FloatPixelData = b'\x00\x01\x02\x03' + ds.save_as(fp) + assert fp.getvalue() == ( + # Tag | VR | Length | Value + b'\xe0\x7f\x08\x00\x4F\x46\x00\x00\x04\x00\x00\x00\x00\x01\x02\x03' + ) + + +class TestWritePN: """Test filewriter.write_PN""" def test_no_encoding(self): @@ -2275,7 +2359,7 @@ assert encoded == fp.getvalue() # regression test: make sure no warning is issued, e.g. the - # PersonName3 value has not saved the default encoding + # PersonName value has not saved the default encoding fp = DicomBytesIO() fp.is_little_endian = True with pytest.warns(None) as warnings: @@ -2313,7 +2397,7 @@ assert encoded == fp.getvalue() -class TestWriteText(object): +class TestWriteText: """Test filewriter.write_PN""" def teardown(self): @@ -2456,7 +2540,7 @@ assert decoded == convert_text(encoded, encodings) -class TestWriteDT(object): +class TestWriteDT: """Test filewriter.write_DT""" def test_format_dt(self): @@ -2474,7 +2558,7 @@ assert _format_DT(elem.value) == '20010203123456' -class TestWriteUndefinedLengthPixelData(object): +class TestWriteUndefinedLengthPixelData: """Test write_data_element() for pixel data with undefined length.""" def setup(self): diff -Nru pydicom-1.4.1/pydicom/tests/test_fixes.py pydicom-2.0.0/pydicom/tests/test_fixes.py --- pydicom-1.4.1/pydicom/tests/test_fixes.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_fixes.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,237 +0,0 @@ -# Copyright 2008-2018 pydicom authors. See LICENSE file for details. -"""Unit tests for pydicom.util.fixes module.""" - -import copy -import pickle - -import datetime as datetime_module -from datetime import datetime -from datetime import timedelta -from datetime import tzinfo - -import pytest - -import pydicom as pydicom_module -from pydicom import compat -from pydicom.util.fixes import timezone - -pickle_choices = [ - (pickle, pickle, proto) for proto in range(pickle.HIGHEST_PROTOCOL + 1) -] - -ZERO = timedelta(0) -HOUR = timedelta(hours=1) -# In the US, DST starts at 2am (standard time) on the first Sunday in April. -DSTSTART = datetime(1, 4, 1, 2) -# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct, -# which is the first Sunday on or after Oct 25. Because we view 1:MM as -# being standard time on that day, there is no spelling in local time of -# the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time). -DSTEND = datetime(1, 10, 25, 1) - - -def first_sunday_on_or_after(dt): - days_to_go = 6 - dt.weekday() - if days_to_go: - dt += timedelta(days_to_go) - return dt - - -class USTimeZone(tzinfo): - def __init__(self, hours, reprname, stdname, dstname): - self.stdoffset = timedelta(hours=hours) - self.reprname = reprname - self.stdname = stdname - self.dstname = dstname - - def __repr__(self): - return self.reprname - - def tzname(self, dt): - if self.dst(dt): - return self.dstname - else: - return self.stdname - - def utcoffset(self, dt): - return self.stdoffset + self.dst(dt) - - def dst(self, dt): - if dt is None or dt.tzinfo is None: - # An exception instead may be sensible here, in one or more of - # the cases. - return ZERO - assert dt.tzinfo is self - - # Find first Sunday in April. - start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) - assert start.weekday() == 6 and start.month == 4 and start.day <= 7 - - # Find last Sunday in October. - end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) - assert end.weekday() == 6 and end.month == 10 and end.day >= 25 - - # Can't compare naive to aware objects, so strip the timezone from - # dt first. - if start <= dt.replace(tzinfo=None) < end: - return HOUR - else: - return ZERO - - -Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") - - -@pytest.mark.skipif(not compat.in_py2, reason='Only test backport in Python 2') -class TestTimeZone(object): - """Backport of datetime.timezone tests. - - Notes - ----- - Backport of datetime.timezone for Python 2.7, from Python 3.6 - documentation (https://tinyurl.com/z4cegu9), copyright Python Software - Foundation (https://docs.python.org/3/license.html) - - """ - def setup(self): - self.ACDT = timezone(timedelta(hours=9.5), 'ACDT') - self.EST = timezone(-timedelta(hours=5), 'EST') - self.DT = datetime(2010, 1, 1) - - def test_str(self): - for tz in [self.ACDT, self.EST, timezone.utc, - timezone.min, timezone.max]: - assert tz.tzname(None) == str(tz) - - def test_repr(self): - datetime = datetime_module - pydicom = pydicom_module - for tz in [self.ACDT, self.EST, timezone.utc, - timezone.min, timezone.max]: - # test round-trip - tzrep = repr(tz) - assert tz == eval(tzrep) - - def test_class_members(self): - limit = timedelta(hours=23, minutes=59) - assert ZERO == timezone.utc.utcoffset(None) - assert -limit == timezone.min.utcoffset(None) - assert limit == timezone.max.utcoffset(None) - - def test_constructor(self): - assert timezone.utc is timezone(timedelta(0)) - assert timezone.utc is not timezone(timedelta(0), 'UTC') - assert timezone(timedelta(0), 'UTC') == timezone.utc - # invalid offsets - for invalid in [timedelta(microseconds=1), timedelta(1, 1), - timedelta(seconds=1), timedelta(1), -timedelta(1)]: - with pytest.raises(ValueError): - timezone(invalid) - with pytest.raises(ValueError): - timezone(-invalid) - - with pytest.raises(TypeError): - timezone(None) - with pytest.raises(TypeError): - timezone(42) - with pytest.raises(TypeError): - timezone(ZERO, None) - with pytest.raises(TypeError): - timezone(ZERO, 42) - with pytest.raises(TypeError): - timezone(ZERO, 'ABC', 'extra') - - def test_inheritance(self): - assert isinstance(timezone.utc, tzinfo) - assert isinstance(self.EST, tzinfo) - - def test_utcoffset(self): - dummy = self.DT - for h in [0, 1.5, 12]: - offset = h * HOUR.total_seconds() - offset = timedelta(seconds=offset) - assert offset == timezone(offset).utcoffset(dummy) - assert -offset == timezone(-offset).utcoffset(dummy) - - with pytest.raises(TypeError): - self.EST.utcoffset('') - with pytest.raises(TypeError): - self.EST.utcoffset(5) - - def test_dst(self): - assert timezone.utc.dst(self.DT) is None - - with pytest.raises(TypeError): - self.EST.dst('') - with pytest.raises(TypeError): - self.EST.dst(5) - - def test_tzname(self): - assert 'UTC' in timezone.utc.tzname(None) - assert 'UTC' in timezone(ZERO).tzname(None) - assert 'UTC-05:00' == timezone(timedelta(hours=-5)).tzname(None) - assert 'UTC+09:30' == timezone(timedelta(hours=9.5)).tzname(None) - assert 'UTC-00:01' == timezone(timedelta(minutes=-1)).tzname(None) - assert 'XYZ' == timezone(-5 * HOUR, 'XYZ').tzname(None) - - with pytest.raises(TypeError): - self.EST.tzname('') - with pytest.raises(TypeError): - self.EST.tzname(5) - - def test_fromutc(self): - with pytest.raises(ValueError): - timezone.utc.fromutc(self.DT) - with pytest.raises(TypeError): - timezone.utc.fromutc('not datetime') - for tz in [self.EST, self.ACDT, Eastern]: - utctime = self.DT.replace(tzinfo=tz) - local = tz.fromutc(utctime) - assert local - utctime == tz.utcoffset(local) - assert local == self.DT.replace(tzinfo=timezone.utc) - - def test_comparison(self): - assert timezone(ZERO) != timezone(HOUR) - assert timezone(HOUR) == timezone(HOUR) - assert timezone(-5 * HOUR) == timezone(-5 * HOUR, 'EST') - with pytest.raises(TypeError): - timezone(ZERO) < timezone(ZERO) - assert timezone(ZERO) in {timezone(ZERO)} - assert timezone(ZERO) is not None - assert not timezone(ZERO) is None - assert 'random' != timezone(ZERO) - - def test_aware_datetime(self): - # test that timezone instances can be used by datetime - t = datetime(1, 1, 1) - for tz in [timezone.min, timezone.max, timezone.utc]: - print(tz.tzname(t)) - assert t.replace(tzinfo=tz).tzname() == tz.tzname(t) - assert t.replace(tzinfo=tz).utcoffset() == tz.utcoffset(t) - assert t.replace(tzinfo=tz).dst() == tz.dst(t) - - def test_pickle(self): - for tz in self.ACDT, self.EST, timezone.min, timezone.max: - for pickler, unpickler, proto in pickle_choices: - tz_copy = unpickler.loads(pickler.dumps(tz, proto)) - assert tz == tz_copy - tz = timezone.utc - for pickler, unpickler, proto in pickle_choices: - tz_copy = unpickler.loads(pickler.dumps(tz, proto)) - assert tz_copy is tz - - def test_copy(self): - for tz in self.ACDT, self.EST, timezone.min, timezone.max: - tz_copy = copy.copy(tz) - assert tz == tz_copy - tz = timezone.utc - tz_copy = copy.copy(tz) - assert tz_copy is tz - - def test_deepcopy(self): - for tz in self.ACDT, self.EST, timezone.min, timezone.max: - tz_copy = copy.deepcopy(tz) - assert tz == tz_copy - tz = timezone.utc - tz_copy = copy.deepcopy(tz) - assert tz_copy is tz diff -Nru pydicom-1.4.1/pydicom/tests/test_gdcm_pixel_data.py pydicom-2.0.0/pydicom/tests/test_gdcm_pixel_data.py --- pydicom-1.4.1/pydicom/tests/test_gdcm_pixel_data.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_gdcm_pixel_data.py 2020-05-29 01:44:31.000000000 +0000 @@ -14,7 +14,6 @@ from pydicom.data import get_testdata_files from pydicom.pixel_data_handlers.util import _convert_YBR_FULL_to_RGB from pydicom.tag import Tag -from pydicom import compat gdcm_missing_message = "GDCM is not available in this test environment" gdcm_im_missing_message = "GDCM is not available or in-memory decoding"\ @@ -105,18 +104,11 @@ save_dir = os.getcwd() -class TestGDCM_JPEG_LS_no_gdcm(object): +class TestGDCM_JPEG_LS_no_gdcm: def setup(self): - if compat.in_py2: - self.utf8_filename = os.path.join( - tempfile.gettempdir(), "ДИКОМ.dcm") - self.unicode_filename = self.utf8_filename.decode("utf8") - shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"), - self.unicode_filename) - else: - self.unicode_filename = os.path.join( - tempfile.gettempdir(), "ДИКОМ.dcm") - shutil.copyfile(jpeg_ls_lossless_name, self.unicode_filename) + self.unicode_filename = os.path.join( + tempfile.gettempdir(), "ДИКОМ.dcm") + shutil.copyfile(jpeg_ls_lossless_name, self.unicode_filename) self.jpeg_ls_lossless = dcmread(self.unicode_filename) self.mr_small = dcmread(mr_name) self.emri_jpeg_ls_lossless = dcmread(emri_jpeg_ls_lossless) @@ -137,7 +129,7 @@ self.emri_jpeg_ls_lossless.pixel_array -class TestGDCM_JPEG2000_no_gdcm(object): +class TestGDCM_JPEG2000_no_gdcm: def setup(self): self.jpeg_2k = dcmread(jpeg2000_name) self.jpeg_2k_lossless = dcmread(jpeg2000_lossless_name) @@ -175,7 +167,7 @@ self.sc_rgb_jpeg2k_gdcm_KY.pixel_array -class TestGDCM_JPEGlossy_no_gdcm(object): +class TestGDCM_JPEGlossy_no_gdcm: def setup(self): self.jpeg_lossy = dcmread(jpeg_lossy_name) self.color_3d_jpeg = dcmread(color_3d_jpeg_baseline) @@ -200,7 +192,7 @@ self.color_3d_jpeg.pixel_array -class TestGDCM_JPEGlossless_no_gdcm(object): +class TestGDCM_JPEGlossless_no_gdcm: def setup(self): self.jpeg_lossless = dcmread(jpeg_lossless_name) self.original_handlers = pydicom.config.pixel_data_handlers @@ -382,7 +374,7 @@ with_gdcm_params = [] -class TestsWithGDCM(object): +class TestsWithGDCM: @pytest.fixture(params=with_gdcm_params, scope='class', autouse=True) def with_gdcm(self, request): original_value = HAVE_GDCM_IN_MEMORY_SUPPORT @@ -396,15 +388,9 @@ @pytest.fixture(scope='class') def unicode_filename(self): - if compat.in_py2: - utf8_filename = os.path.join(tempfile.gettempdir(), "ДИКОМ.dcm") - unicode_filename = utf8_filename.decode("utf8") - shutil.copyfile(jpeg_ls_lossless_name.decode("utf8"), - unicode_filename) - else: - unicode_filename = os.path.join( - tempfile.gettempdir(), "ДИКОМ.dcm") - shutil.copyfile(jpeg_ls_lossless_name, unicode_filename) + unicode_filename = os.path.join( + tempfile.gettempdir(), "ДИКОМ.dcm") + shutil.copyfile(jpeg_ls_lossless_name, unicode_filename) yield unicode_filename os.remove(unicode_filename) @@ -587,7 +573,7 @@ assert PhotometricInterpretation == t.PhotometricInterpretation -class TestSupportFunctions(object): +class TestSupportFunctions: @pytest.fixture(scope='class') def dataset_2d(self): return dcmread(mr_name) @@ -685,11 +671,3 @@ image_reader = gdcm_handler.create_image_reader(mr_name) assert image_reader is not None assert image_reader.Read() - - @pytest.mark.skipif(not HAVE_GDCM, reason=gdcm_missing_message) - @pytest.mark.skipif(not compat.in_py2, reason='Python2 specific') - def test_create_image_reader_with_py2_unicode_string(self): - filename = mr_name.decode('utf-8') - image_reader = gdcm_handler.create_image_reader(filename) - assert image_reader is not None - assert image_reader.Read() diff -Nru pydicom-1.4.1/pydicom/tests/test_handler_util.py pydicom-2.0.0/pydicom/tests/test_handler_util.py --- pydicom-1.4.1/pydicom/tests/test_handler_util.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_handler_util.py 2020-05-29 01:44:31.000000000 +0000 @@ -15,7 +15,7 @@ from pydicom import dcmread from pydicom.data import get_testdata_files, get_palette_files -from pydicom.dataset import Dataset +from pydicom.dataset import Dataset, FileMetaDataset from pydicom.pixel_data_handlers.util import ( dtype_corrected_for_endianness, reshape_pixel_array, @@ -61,7 +61,7 @@ # Tests with Numpy unavailable @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy(object): +class TestNoNumpy: """Tests for the util functions without numpy.""" def test_pixel_dtype_raises(self): """Test that pixel_dtype raises exception without numpy.""" @@ -88,12 +88,12 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_PixelDtype(object): +class TestNumpy_PixelDtype: """Tests for util.pixel_dtype.""" def setup(self): """Setup the test dataset.""" self.ds = Dataset() - self.ds.file_meta = Dataset() + self.ds.file_meta = FileMetaDataset() self.ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian def test_unknown_pixel_representation_raises(self): @@ -275,12 +275,12 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_ReshapePixelArray(object): +class TestNumpy_ReshapePixelArray: """Tests for util.reshape_pixel_array.""" def setup(self): """Setup the test dataset.""" self.ds = Dataset() - self.ds.file_meta = Dataset() + self.ds.file_meta = FileMetaDataset() self.ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian self.ds.Rows = 4 self.ds.Columns = 5 @@ -527,7 +527,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_ConvertColourSpace(object): +class TestNumpy_ConvertColourSpace: """Tests for util.convert_color_space.""" def test_unknown_current_raises(self): """Test an unknown current color space raises exception.""" @@ -635,7 +635,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_DtypeCorrectedForEndianness(object): +class TestNumpy_DtypeCorrectedForEndianness: """Tests for util.dtype_corrected_for_endianness.""" def test_byte_swapping(self): """Test that the endianess of the system is taken into account.""" @@ -736,7 +736,7 @@ ] -class TestGetExpectedLength(object): +class TestGetExpectedLength: """Tests for util.get_expected_length.""" @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH) def test_length_in_bytes(self, shape, bits, length): @@ -785,7 +785,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_ModalityLUT(object): +class TestNumpy_ModalityLUT: """Tests for util.apply_modality_lut().""" def test_slope_intercept(self): """Test the rescale slope/intercept transform.""" @@ -858,9 +858,39 @@ out = apply_modality_lut(arr, ds) assert arr is out + def test_lutdata_ow(self): + """Test LUT Data with VR OW.""" + ds = dcmread(MOD_16_SEQ) + assert ds.is_little_endian is True + seq = ds.ModalityLUTSequence[0] + assert [4096, -2048, 16] == seq.LUTDescriptor + seq.LUTData = pack('<4096H', *seq.LUTData) + seq['LUTData'].VR = 'OW' + arr = ds.pixel_array + assert -2048 == arr.min() + assert 4095 == arr.max() + out = apply_modality_lut(arr, ds) + + # IV > 2047 -> LUT[4095] + mapped_pixels = arr > 2047 + assert 65535 == out[mapped_pixels][0] + assert (65535 == out[mapped_pixels]).all() + assert out.flags.writeable + assert out.dtype == np.uint16 + + assert [65535, 65535, 49147, 49147, 65535] == list(out[0, 50:55]) + assert [65535, 65535, 65535, 65535, 65535] == list(out[50, 50:55]) + assert [65535, 65535, 65535, 65535, 65535] == list(out[100, 50:55]) + assert [65535, 65535, 49147, 49147, 65535] == list(out[150, 50:55]) + assert [65535, 65535, 49147, 49147, 65535] == list(out[200, 50:55]) + assert 39321 == out[185, 340] + assert 45867 == out[185, 385] + assert 52428 == out[228, 385] + assert 58974 == out[291, 385] + @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_PaletteColor(object): +class TestNumpy_PaletteColor: """Tests for util.apply_color_lut().""" def setup(self): """Setup the tests""" @@ -960,7 +990,7 @@ def test_uint08_16(self): """Test uint8 Pixel Data with 16-bit LUT entries.""" ds = dcmread(PAL_08_200_0_16_1F, force=True) - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian assert 8 == ds.BitsStored assert 16 == ds.RedPaletteColorLookupTableDescriptor[2] @@ -1053,7 +1083,7 @@ def test_16_allocated_8_entries(self): """Test LUT with 8-bit entries in 16 bits allocated.""" ds = dcmread(PAL_08_200_0_16_1F, force=True) - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian ds.RedPaletteColorLookupTableDescriptor = [200, 0, 8] lut = pack('<200H', *list(range(0, 200))) @@ -1117,7 +1147,7 @@ def test_first_map_positive(self): """Test a positive first mapping value.""" ds = dcmread(PAL_08_200_0_16_1F, force=True) - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian ds.RedPaletteColorLookupTableDescriptor[1] = 10 arr = ds.pixel_array @@ -1135,7 +1165,7 @@ def test_first_map_negative(self): """Test a positive first mapping value.""" ds = dcmread(PAL_08_200_0_16_1F, force=True) - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian ds.RedPaletteColorLookupTableDescriptor[1] = -10 arr = ds.pixel_array @@ -1150,9 +1180,18 @@ assert [60160, 25600, 37376] == list(rgb[arr == 130][0]) assert ([60160, 25600, 37376] == rgb[arr == 130]).all() + def test_unchanged(self): + """Test dataset with no LUT is unchanged.""" + # Regression test for #1068 + ds = dcmread(MOD_16, force=True) + assert 'RedPaletteColorLookupTableDescriptor' not in ds + msg = r"No suitable Palette Color Lookup Table Module found" + with pytest.raises(ValueError, match=msg): + apply_color_lut(ds.pixel_array, ds) + @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_ExpandSegmentedLUT(object): +class TestNumpy_ExpandSegmentedLUT: """Tests for util._expand_segmented_lut().""" def test_discrete(self): """Test expanding a discrete segment.""" @@ -1373,7 +1412,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_VOILUT(object): +class TestNumpy_VOILUT: """Tests for util.apply_voi_lut().""" def test_voi_single_view(self): """Test VOI LUT with a single view.""" @@ -1916,3 +1955,21 @@ arr = np.asarray([-128, -127, -1, 0, 1, 126, 127], dtype='int8') out = apply_voi_lut(arr, ds) assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist() + + def test_voi_lutdata_ow(self): + """Test LUT Data with VR OW.""" + ds = Dataset() + ds.is_little_endian = True + ds.is_explicit_VR = True + ds.PixelRepresentation = 0 + ds.BitsStored = 16 + ds.VOILUTSequence = [Dataset()] + item = ds.VOILUTSequence[0] + item.LUTDescriptor = [4, 0, 16] + item.LUTData = [0, 127, 32768, 65535] + item.LUTData = pack('<4H', *item.LUTData) + item['LUTData'].VR = 'OW' + arr = np.asarray([0, 1, 2, 3, 255], dtype='uint16') + out = apply_voi_lut(arr, ds) + assert 'uint16' == out.dtype + assert [0, 127, 32768, 65535, 65535] == out.tolist() diff -Nru pydicom-1.4.1/pydicom/tests/test_jpeg_ls_pixel_data.py pydicom-2.0.0/pydicom/tests/test_jpeg_ls_pixel_data.py --- pydicom-1.4.1/pydicom/tests/test_jpeg_ls_pixel_data.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_jpeg_ls_pixel_data.py 2020-05-29 01:44:31.000000000 +0000 @@ -67,7 +67,7 @@ 'jpegls', 'jpeg_ls', 'JPEG_LS', 'jpegls_handler', 'JPEG_LS_Handler' ) -class TestJPEGLS_no_jpeg_ls(object): +class TestJPEGLS_no_jpeg_ls: def setup(self): self.jpeg_ls_lossless = dcmread(jpeg_ls_lossless_name) self.mr_small = dcmread(mr_name) @@ -84,7 +84,7 @@ self.jpeg_ls_lossless.pixel_array -class TestJPEGLS_JPEG2000_no_jpeg_ls(object): +class TestJPEGLS_JPEG2000_no_jpeg_ls: def setup(self): self.jpeg_2k = dcmread(jpeg2000_name) self.jpeg_2k_lossless = dcmread(jpeg2000_lossless_name) @@ -108,7 +108,7 @@ self.emri_jpeg_2k_lossless.pixel_array -class TestJPEGLS_JPEGlossy_no_jpeg_ls(object): +class TestJPEGLS_JPEGlossy_no_jpeg_ls: def setup(self): self.jpeg_lossy = dcmread(jpeg_lossy_name) self.color_3d_jpeg = dcmread(color_3d_jpeg_baseline) @@ -133,7 +133,7 @@ self.color_3d_jpeg.pixel_array -class TestJPEGLS_JPEGlossless_no_jpeg_ls(object): +class TestJPEGLS_JPEGlossless_no_jpeg_ls: def setup(self): self.jpeg_lossless = dcmread(jpeg_lossless_name) self.original_handlers = pydicom.config.pixel_data_handlers @@ -157,7 +157,7 @@ @pytest.mark.skipif(not test_jpeg_ls_decoder, reason=jpeg_ls_missing_message) -class TestJPEGLS_JPEG_LS_with_jpeg_ls(object): +class TestJPEGLS_JPEG_LS_with_jpeg_ls: def setup(self): self.jpeg_ls_lossless = dcmread(jpeg_ls_lossless_name) self.mr_small = dcmread(mr_name) @@ -195,7 +195,7 @@ @pytest.mark.skipif(not test_jpeg_ls_decoder, reason=jpeg_ls_missing_message) -class TestJPEGLS_JPEG2000_with_jpeg_ls(object): +class TestJPEGLS_JPEG2000_with_jpeg_ls: def setup(self): self.jpeg_2k = dcmread(jpeg2000_name) self.jpeg_2k_lossless = dcmread(jpeg2000_lossless_name) @@ -218,7 +218,7 @@ @pytest.mark.skipif(not test_jpeg_ls_decoder, reason=jpeg_ls_missing_message) -class TestJPEGLS_JPEGlossy_with_jpeg_ls(object): +class TestJPEGLS_JPEGlossy_with_jpeg_ls: def setup(self): self.jpeg_lossy = dcmread(jpeg_lossy_name) self.color_3d_jpeg = dcmread(color_3d_jpeg_baseline) @@ -243,7 +243,7 @@ @pytest.mark.skipif(not test_jpeg_ls_decoder, reason=jpeg_ls_missing_message) -class TestJPEGLS_JPEGlossless_with_jpeg_ls(object): +class TestJPEGLS_JPEGlossless_with_jpeg_ls: def setup(self): self.jpeg_lossless = dcmread(jpeg_lossless_name) self.original_handlers = pydicom.config.pixel_data_handlers diff -Nru pydicom-1.4.1/pydicom/tests/test_json.py pydicom-2.0.0/pydicom/tests/test_json.py --- pydicom-1.4.1/pydicom/tests/test_json.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_json.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,30 +4,28 @@ import pytest -from pydicom import dcmread, compat +from pydicom import dcmread from pydicom.data import get_testdata_file from pydicom.dataelem import DataElement from pydicom.dataset import Dataset from pydicom.tag import Tag, BaseTag -from pydicom.valuerep import PersonNameUnicode, PersonName3 +from pydicom.valuerep import PersonName -class TestPersonName(object): +class TestPersonName: def test_json_pn_from_file(self): with open(get_testdata_file("test_PN.json")) as s: ds = Dataset.from_json(s.read()) - assert isinstance(ds[0x00080090].value, - (PersonNameUnicode, PersonName3)) - assert isinstance(ds[0x00100010].value, - (PersonNameUnicode, PersonName3)) + assert isinstance(ds[0x00080090].value, PersonName) + assert isinstance(ds[0x00100010].value, PersonName) inner_seq = ds[0x04000561].value[0][0x04000550] dataelem = inner_seq[0][0x00100010] - assert isinstance(dataelem.value, (PersonNameUnicode, PersonName3)) + assert isinstance(dataelem.value, PersonName) def test_pn_components_to_json(self): def check_name(tag, components): # we cannot directly compare the dictionaries, as they are not - # ordered in Python 2 + # guaranteed insertion-ordered in Python < 3.7 value = ds_json[tag]['Value'] assert 1 == len(value) value = value[0] @@ -84,8 +82,6 @@ u'"00091007": {"vr": "PN", "Value": ' u'[{"Alphabetic": "Yamada^Tarou", ' u'"Ideographic": "山田^太郎"}]}}') - if compat.in_py2: - ds_json = ds_json.encode('UTF8') ds = Dataset.from_json(ds_json) assert u'Yamada^Tarou=山田^太郎=やまだ^たろう' == ds.PatientName @@ -118,10 +114,10 @@ vr = "PN" value = [{"Alphabetic": ""}] dataelem = DataElement.from_json(Dataset, tag, vr, value, "Value") - assert isinstance(dataelem.value, (PersonNameUnicode, PersonName3)) + assert isinstance(dataelem.value, PersonName) -class TestAT(object): +class TestAT: def test_to_json(self): ds = Dataset() ds.add_new(0x00091001, 'AT', [0x00100010, 0x00100020]) @@ -162,7 +158,7 @@ assert 0x00100010 == ds[0x00091002].value -class TestDataSetToJson(object): +class TestDataSetToJson: def test_json_from_dicom_file(self): ds1 = Dataset(dcmread(get_testdata_file("CT_small.dcm"))) ds_json = ds1.to_json() @@ -232,11 +228,6 @@ assert ds == ds2 json_model2 = ds.to_json_dict() - if compat.in_py2: - # in Python 2, the encoding of this is slightly different - # (single vs double quotation marks) - del json_model['00091015'] - del json_model2['00091015'] assert json_model == json_model2 @@ -280,7 +271,7 @@ assert ds_json.index('"00100030"') < ds_json.index('"00100040"') -class TestSequence(object): +class TestSequence: def test_nested_sequences(self): test1_json = get_testdata_file("test1.json") with open(test1_json) as f: @@ -292,7 +283,7 @@ assert ds == ds2 -class TestBinary(object): +class TestBinary: def test_inline_binary(self): ds = Dataset() ds.add_new(0x00091002, 'OB', b'BinaryContent') diff -Nru pydicom-1.4.1/pydicom/tests/test_misc.py pydicom-2.0.0/pydicom/tests/test_misc.py --- pydicom-1.4.1/pydicom/tests/test_misc.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_misc.py 2020-05-29 01:44:31.000000000 +0000 @@ -12,7 +12,7 @@ no_meta_file = get_testdata_files('ExplVR_LitEndNoMeta.dcm')[0] -class TestMisc(object): +class TestMisc: def test_is_dicom(self): """Test the is_dicom function.""" invalid_file = test_file.replace('CT_', 'CT') # invalid file diff -Nru pydicom-1.4.1/pydicom/tests/test_multival.py pydicom-2.0.0/pydicom/tests/test_multival.py --- pydicom-1.4.1/pydicom/tests/test_multival.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_multival.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,7 +4,7 @@ import pytest from pydicom.multival import MultiValue from pydicom.valuerep import DS, DSfloat, DSdecimal, IS -from pydicom import config, compat +from pydicom import config from copy import deepcopy import sys @@ -12,7 +12,7 @@ python_version = sys.version_info -class TestMultiValue(object): +class TestMultiValue: def testMultiDS(self): """MultiValue: Multi-valued data elements can be created........""" multival = MultiValue(DS, ['11.1', '22.2', '33.3']) @@ -131,7 +131,7 @@ """MultiValue: test print output""" multival = MultiValue(IS, []) assert '' == str(multival) - multival = MultiValue(compat.text_type, [1, 2, 3]) + multival = MultiValue(str, [1, 2, 3]) assert "['1', '2', '3']" == str(multival) multival = MultiValue(int, [1, 2, 3]) assert '[1, 2, 3]' == str(multival) diff -Nru pydicom-1.4.1/pydicom/tests/test_numpy_pixel_data.py pydicom-2.0.0/pydicom/tests/test_numpy_pixel_data.py --- pydicom-1.4.1/pydicom/tests/test_numpy_pixel_data.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_numpy_pixel_data.py 2020-05-29 01:44:31.000000000 +0000 @@ -33,7 +33,7 @@ import pydicom from pydicom import config from pydicom.data import get_testdata_files -from pydicom.dataset import Dataset +from pydicom.dataset import Dataset, FileMetaDataset from pydicom.filereader import dcmread from pydicom.tests._handler_common import ALL_TRANSFER_SYNTAXES @@ -186,7 +186,7 @@ # Numpy and the numpy handler are unavailable @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_NoNumpyHandler(object): +class TestNoNumpy_NoNumpyHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): @@ -251,7 +251,7 @@ # Numpy unavailable and the numpy handler is available @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_NumpyHandler(object): +class TestNoNumpy_NumpyHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): @@ -322,7 +322,7 @@ # Numpy is available, the numpy handler is unavailable @pytest.mark.skipif(not HAVE_NP, reason='Numpy is unavailable') -class TestNumpy_NoNumpyHandler(object): +class TestNumpy_NoNumpyHandler: """Tests for handling datasets without the handler.""" def setup(self): @@ -420,7 +420,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_NumpyHandler(object): +class TestNumpy_NumpyHandler: """Tests for handling Pixel Data with the handler.""" def setup(self): @@ -1068,7 +1068,7 @@ def test_endianness_not_set(self): """Test for #704, Dataset.is_little_endian unset.""" ds = Dataset() - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian ds.Rows = 10 ds.Columns = 10 @@ -1093,7 +1093,7 @@ # Tests for numpy_handler module with Numpy available @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_GetPixelData(object): +class TestNumpy_GetPixelData: """Tests for numpy_handler.get_pixeldata with numpy.""" def test_no_pixel_data_raises(self): """Test get_pixeldata raises if dataset has no PixelData.""" @@ -1294,7 +1294,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_UnpackBits(object): +class TestNumpy_UnpackBits: """Tests for numpy_handler.unpack_bits.""" @pytest.mark.parametrize('input, output', REFERENCE_PACK_UNPACK) @@ -1325,7 +1325,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_PackBits(object): +class TestNumpy_PackBits: """Tests for numpy_handler.pack_bits.""" @pytest.mark.parametrize('output, input', REFERENCE_PACK_UNPACK) diff -Nru pydicom-1.4.1/pydicom/tests/test_overlay_np.py pydicom-2.0.0/pydicom/tests/test_overlay_np.py --- pydicom-1.4.1/pydicom/tests/test_overlay_np.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_overlay_np.py 2020-05-29 01:44:31.000000000 +0000 @@ -118,7 +118,7 @@ # Numpy is/isn't available, numpy handler is unavailable -class TestNoNumpyHandler(object): +class TestNoNumpyHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): """Setup the environment.""" @@ -141,7 +141,7 @@ # Numpy unavailable and the numpy handler is available @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_NumpyHandler(object): +class TestNoNumpy_NumpyHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): """Setup the environment.""" @@ -199,7 +199,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_NumpyHandler(object): +class TestNumpy_NumpyHandler: """Tests for handling Overlay Data with the handler.""" def setup(self): """Setup the test datasets and the environment.""" @@ -312,7 +312,7 @@ # Tests for numpy_handler module with Numpy available @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_GetOverlayArray(object): +class TestNumpy_GetOverlayArray: """Tests for numpy_handler.get_overlay_array with numpy.""" def test_no_overlay_data_raises(self): """Test get_overlay_array raises if dataset has no OverlayData.""" @@ -432,7 +432,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_ReshapeOverlayArray(object): +class TestNumpy_ReshapeOverlayArray: """Tests for numpy_handler.reshape_overlay_array.""" def setup(self): """Setup the test dataset.""" @@ -537,7 +537,7 @@ @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available") -class TestNumpy_GetExpectedLength(object): +class TestNumpy_GetExpectedLength: """Tests for numpy_handler.get_expected_length.""" @pytest.mark.parametrize('shape, bits, length', REFERENCE_LENGTH) def test_length_in_bytes(self, shape, bits, length): diff -Nru pydicom-1.4.1/pydicom/tests/test_pillow_pixel_data.py pydicom-2.0.0/pydicom/tests/test_pillow_pixel_data.py --- pydicom-1.4.1/pydicom/tests/test_pillow_pixel_data.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_pillow_pixel_data.py 2020-05-29 01:44:31.000000000 +0000 @@ -70,10 +70,6 @@ JPGB_08_08_3_0_1F_RGB = get_testdata_file("SC_rgb_dcmtk_+eb+cr.dcm") # JPGE: 1.2.840.10008.1.2.4.51 - JPEG Extended (Process 2 and 4) (8 and 12-bit) # No supported datasets available -# JPGL: 1.2.840.10008.1.2.4.70 - JPEG Lossless, Non-hierarchical, 1st Order -# No supported datasets available -# JPGL14: 1.2.840.10008.1.2.4.57 - JPEG Lossless P14 -# No supported datasets available # JPEG 2000 - ISO/IEC 15444 Standard # J2KR: 1.2.840.100008.1.2.4.90 - JPEG 2000 Lossless @@ -102,12 +98,16 @@ RLE = get_testdata_file("MR_small_RLE.dcm") JPGE_16_12_1_0_1F_M2 = get_testdata_file("JPEG-lossy.dcm") JPGL_16_16_1_1_1F_M2 = get_testdata_file("JPEG-LL.dcm") +# JPGL14: 1.2.840.10008.1.2.4.57 - JPEG Lossless P14 +# No datasets available +# JPGL: 1.2.840.10008.1.2.4.70 - JPEG Lossless, Non-hierarchical, 1st Order +JPGL_08_08_1_0_1F = get_testdata_file("JPGLosslessP14SV1_1s_1f_8b.dcm") # Transfer Syntaxes (non-retired + Explicit VR Big Endian) JPEG_SUPPORTED_SYNTAXES = [] if HAVE_JPEG: - JPEG_SUPPORTED_SYNTAXES = [JPEGBaseline, JPEGExtended, JPEGLossless] + JPEG_SUPPORTED_SYNTAXES = [JPEGBaseline, JPEGExtended] JPEG2K_SUPPORTED_SYNTAXES = [] if HAVE_JPEG2K: @@ -131,13 +131,14 @@ (EXPB, ('1.2.840.10008.1.2.2', 'OB^^^^')), (DEFL, ('1.2.840.10008.1.2.1.99', '^^^^')), (JPEG_LS_LOSSLESS, ('1.2.840.10008.1.2.4.80', 'CompressedSamples^MR1')), + (JPGL_08_08_1_0_1F, ('1.2.840.10008.1.2.4.70', 'Citizen^Jan')), (RLE, ('1.2.840.10008.1.2.5', 'CompressedSamples^MR1')), ] # Numpy and the pillow handler are unavailable @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_NoPillowHandler(object): +class TestNoNumpy_NoPillowHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): @@ -377,7 +378,7 @@ @pytest.mark.skipif(not HAVE_JPEG2K, reason='Pillow or JPEG2K not available') -class TestPillowHandler_JPEG2K(object): +class TestPillowHandler_JPEG2K: """Tests for handling Pixel Data with the handler.""" def setup(self): """Setup the test datasets and the environment.""" @@ -485,7 +486,7 @@ @pytest.mark.skipif(not HAVE_JPEG, reason='Pillow or JPEG not available') -class TestPillowHandler_JPEG(object): +class TestPillowHandler_JPEG: """Tests for handling Pixel Data with the handler.""" def setup(self): """Setup the test datasets and the environment.""" @@ -578,15 +579,18 @@ def test_JPGE_16bit_raises(self): """Test decoding JPEG lossy with pillow handler fails.""" ds = dcmread(JPGE_16_12_1_0_1F_M2) - msg = r"JPEG Lossy only supported if Bits Allocated = 8" + msg = ( + r"1.2.840.10008.1.2.4.51 - JPEG Extended \(Process 2 and 4\) only " + r"supported by Pillow if Bits Allocated = 8" + ) with pytest.raises(NotImplementedError, match=msg): ds.pixel_array def test_JPGL_raises(self): - """Test decoding JPEG lossless with pillow handler fails.""" + """Test decoding JPEG Lossless with pillow handler fails.""" ds = dcmread(JPGL_16_16_1_1_1F_M2) - msg = r"cannot identify image file" - with pytest.raises((IOError, OSError), match=msg): + msg = r"as there are no pixel data handlers available that support it" + with pytest.raises(NotImplementedError, match=msg): ds.pixel_array def test_JPGB_odd_data_size(self): @@ -597,7 +601,7 @@ assert pixel_data.shape == (3, 3, 3) -class TestPillow_GetJ2KPrecision(object): +class TestPillow_GetJ2KPrecision: """Tests for _get_j2k_precision.""" def test_precision(self): """Test getting the precision for a JPEG2K bytestream.""" diff -Nru pydicom-1.4.1/pydicom/tests/test_rawread.py pydicom-2.0.0/pydicom/tests/test_rawread.py --- pydicom-1.4.1/pydicom/tests/test_rawread.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_rawread.py 2020-05-29 01:44:31.000000000 +0000 @@ -11,7 +11,7 @@ from pydicom.util.hexutil import hex2bytes -class TestRawReaderExplVRTests(object): +class TestRawReaderExplVRTests: # See comments in data_element_generator # summary of DICOM data element formats # Here we are trying to test all those variations @@ -78,7 +78,7 @@ assert got.value.startswith(b'ABCDEFGHIJ\0') -class TestRawReaderImplVR(object): +class TestRawReaderImplVR: # See comments in data_element_generator # summary of DICOM data element formats # Here we are trying to test all those variations @@ -128,7 +128,7 @@ assert got.value.startswith(b'ABCDEFGHIJ\0') -class TestRawSequence(object): +class TestRawSequence: # See DICOM standard PS3.5-2008 section 7.5 for sequence syntax def testEmptyItem(self): """Read sequence with a single empty item...""" diff -Nru pydicom-1.4.1/pydicom/tests/test_rle_pixel_data.py pydicom-2.0.0/pydicom/tests/test_rle_pixel_data.py --- pydicom-1.4.1/pydicom/tests/test_rle_pixel_data.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_rle_pixel_data.py 2020-05-29 01:44:31.000000000 +0000 @@ -30,6 +30,7 @@ from pydicom import dcmread, Dataset, config import pydicom.config from pydicom.data import get_testdata_files +from pydicom.dataset import FileMetaDataset from pydicom.encaps import defragment_data from pydicom.uid import RLELossless, UID from pydicom.tests._handler_common import ALL_TRANSFER_SYNTAXES @@ -188,7 +189,7 @@ # Numpy and the RLE handler are unavailable @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_NoRLEHandler(object): +class TestNoNumpy_NoRLEHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): """Setup the environment.""" @@ -233,7 +234,7 @@ # Numpy unavailable and the RLE handler is available @pytest.mark.skipif(HAVE_NP, reason='Numpy is available') -class TestNoNumpy_RLEHandler(object): +class TestNoNumpy_RLEHandler: """Tests for handling datasets without numpy and the handler.""" def setup(self): """Setup the environment.""" @@ -291,7 +292,7 @@ # Numpy is available, the RLE handler is unavailable @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_NoRLEHandler(object): +class TestNumpy_NoRLEHandler: """Tests for handling datasets with no handler.""" def setup(self): """Setup the environment.""" @@ -336,7 +337,7 @@ # Numpy and the RLE handler are available @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEHandler(object): +class TestNumpy_RLEHandler: """Tests for handling datasets with the handler.""" def setup(self): """Setup the environment.""" @@ -716,7 +717,7 @@ # Tests for rle_handler module with Numpy available @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_GetPixelData(object): +class TestNumpy_GetPixelData: """Tests for rle_handler.get_pixeldata with numpy.""" def test_no_pixel_data_raises(self): """Test get_pixeldata raises if dataset has no PixelData.""" @@ -821,7 +822,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEParseHeader(object): +class TestNumpy_RLEParseHeader: """Tests for rle_handler._parse_rle_header.""" def test_invalid_header_length(self): """Test exception raised if header is not 64 bytes long.""" @@ -850,7 +851,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEDecodeFrame(object): +class TestNumpy_RLEDecodeFrame: """Tests for rle_handler._rle_decode_frame.""" def test_unsupported_bits_allocated_raises(self): """Test exception raised for BitsAllocated not a multiple of 8.""" @@ -1047,7 +1048,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEDecodeSegment(object): +class TestNumpy_RLEDecodeSegment: """Tests for rle_handler._rle_decode_segment. Using int8 @@ -1195,7 +1196,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEEncodeRow(object): +class TestNumpy_RLEEncodeRow: """Tests for rle_handler._rle_encode_row.""" @pytest.mark.parametrize('input, output', REFERENCE_ENCODE_ROW) def test_encode(self, input, output): @@ -1204,13 +1205,13 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEEncodeFrame(object): +class TestNumpy_RLEEncodeFrame: """Tests for rle_handler.rle_encode_frame.""" def setup(self): """Setup the tests.""" # Create a dataset skeleton for use in the cycle tests ds = Dataset() - ds.file_meta = Dataset() + ds.file_meta = FileMetaDataset() ds.file_meta.TransferSyntaxUID = '1.2.840.10008.1.2' ds.Rows = 2 ds.Columns = 4 @@ -1416,7 +1417,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEEncodePlane(object): +class TestNumpy_RLEEncodePlane: """Tests for rle_handler._rle_encode_plane.""" def test_8bit(self): """Test encoding an 8-bit plane into 1 segment.""" @@ -1629,7 +1630,7 @@ @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available') -class TestNumpy_RLEEncodeSegment(object): +class TestNumpy_RLEEncodeSegment: """Tests for rle_handler._rle_encode_segment.""" def test_one_row(self): """Test encoding data that contains only a single row.""" diff -Nru pydicom-1.4.1/pydicom/tests/test_sequence.py pydicom-2.0.0/pydicom/tests/test_sequence.py --- pydicom-1.4.1/pydicom/tests/test_sequence.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_sequence.py 2020-05-29 01:44:31.000000000 +0000 @@ -7,7 +7,7 @@ from pydicom.sequence import Sequence -class TestSequence(object): +class TestSequence: def testDefaultInitialization(self): """Sequence: Ensure a valid Sequence is created""" empty = Sequence() diff -Nru pydicom-1.4.1/pydicom/tests/test_tag.py pydicom-2.0.0/pydicom/tests/test_tag.py --- pydicom-1.4.1/pydicom/tests/test_tag.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_tag.py 2020-05-29 01:44:31.000000000 +0000 @@ -3,11 +3,10 @@ import pytest -from pydicom.compat import in_py2 from pydicom.tag import BaseTag, Tag, TupleTag, tag_in_exception -class TestBaseTag(object): +class TestBaseTag: """Test the BaseTag class.""" def test_le_same_class(self): """Test __le__ of two classes with same type.""" @@ -241,18 +240,8 @@ assert BaseTag(0x000900FF).is_private_creator assert not BaseTag(0x00090100).is_private_creator - def test_base_class(self): - """Test the class BaseTag inherits from.""" - if in_py2: - # Test for overflow of int - tag = Tag(0xFFFFFFFF) - assert isinstance(tag, long) - else: - tag = Tag(0xFFFFFFFF) - assert isinstance(tag, int) - -class TestTag(object): +class TestTag: """Test the Tag method.""" def test_tag_single_int(self): """Test creating a Tag from a single int.""" @@ -323,12 +312,6 @@ pytest.raises(ValueError, Tag, ['0x01', '0x02'], '0x01') pytest.raises(ValueError, Tag, ['0x01', '0x02'], 0x01) - @pytest.mark.skipif(not in_py2, reason='Long type only exists in Python 2') - def test_mixed_long_int(self): - assert Tag([0x1000, long(0x2000)]) == BaseTag(0x10002000) - assert Tag([long(0x1000), 0x2000]) == BaseTag(0x10002000) - assert Tag([long(0x1000), long(0x2000)]) == BaseTag(0x10002000) - def test_tag_single_str(self): """Test creating a Tag from a single str.""" assert Tag('0x10002000') == BaseTag(0x10002000) @@ -380,14 +363,14 @@ pytest.raises(ValueError, Tag, -65535, -1) -class TestTupleTag(object): +class TestTupleTag: """Test the TupleTag method.""" def test_tuple_tag(self): """Test quick tag construction with TupleTag.""" assert TupleTag((0xFFFF, 0xFFee)) == BaseTag(0xFFFFFFEE) -class TestTagInException(object): +class TestTagInException: """Test the tag_in_exception method.""" def test_raise_exception(self): """""" diff -Nru pydicom-1.4.1/pydicom/tests/test_uid.py pydicom-2.0.0/pydicom/tests/test_uid.py --- pydicom-1.4.1/pydicom/tests/test_uid.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_uid.py 2020-05-29 01:44:31.000000000 +0000 @@ -8,7 +8,7 @@ from pydicom.uid import UID, generate_uid, PYDICOM_ROOT_UID, JPEGLSLossy -class TestGenerateUID(object): +class TestGenerateUID: def test_generate_uid(self): """Test UID generator""" # Test standard UID generation with pydicom prefix @@ -78,7 +78,7 @@ assert uid.is_valid -class TestUID(object): +class TestUID: """Test DICOM UIDs""" @classmethod def setup_class(self): @@ -291,7 +291,7 @@ assert a == b -class TestUIDPrivate(object): +class TestUIDPrivate: """Test private UIDs""" @classmethod def setup_class(self): diff -Nru pydicom-1.4.1/pydicom/tests/test_unicode.py pydicom-2.0.0/pydicom/tests/test_unicode.py --- pydicom-1.4.1/pydicom/tests/test_unicode.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_unicode.py 2020-05-29 01:44:31.000000000 +0000 @@ -9,7 +9,7 @@ from pydicom import dcmread -class TestUnicodeFilenames(object): +class TestUnicodeFilenames: def test_read(self): """Unicode: Can read a file with unicode characters in name...""" uni_name = u'test°' diff -Nru pydicom-1.4.1/pydicom/tests/test_util.py pydicom-2.0.0/pydicom/tests/test_util.py --- pydicom-1.4.1/pydicom/tests/test_util.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_util.py 2020-05-29 01:44:31.000000000 +0000 @@ -21,13 +21,18 @@ from pydicom.util.hexutil import hex2bytes, bytes2hex from pydicom.data import get_testdata_files +have_numpy = True +try: + import numpy +except ImportError: + have_numpy = False test_dir = os.path.dirname(__file__) raw_hex_module = os.path.join(test_dir, '_write_stds.py') raw_hex_code = open(raw_hex_module, "rb").read() -class TestCodify(object): +class TestCodify: """Test the utils.codify module""" def test_camel_to_underscore(self): """Test utils.codify.camel_to_underscore""" @@ -54,10 +59,8 @@ def test_code_imports(self): """Test utils.codify.code_imports""" - out = "from __future__ import unicode_literals" - out += " # Only for python2.7 and save_as unicode filename\n" - out += 'import pydicom\n' - out += 'from pydicom.dataset import Dataset\n' + out = 'import pydicom\n' + out += 'from pydicom.dataset import Dataset, FileMetaDataset\n' out += 'from pydicom.sequence import Sequence' assert out == code_imports() @@ -140,7 +143,7 @@ assert r"c:\temp\testout.dcm" in out -class TestDump(object): +class TestDump: """Test the utils.dump module""" def test_print_character(self): """Test utils.dump.print_character""" @@ -170,7 +173,7 @@ pass -class TestFixer(object): +class TestFixer: """Test the utils.fixer module""" def test_fix_separator_callback(self): """Test utils.fixer.fix_separator_callback""" @@ -189,7 +192,7 @@ pass -class TestHexUtil(object): +class TestHexUtil: """Test the utils.hexutil module""" def test_hex_to_bytes(self): """Test utils.hexutil.hex2bytes""" @@ -224,7 +227,7 @@ assert hexstring == bytes2hex(bytestring) -class TestDataElementCallbackTests(object): +class TestDataElementCallbackTests: def setup(self): # Set up a dataset with commas in one item instead of backslash config.enforce_valid_values = True @@ -255,8 +258,11 @@ process_unknown_VRs=False) ds = filereader.read_dataset(self.bytesio, is_little_endian=True, is_implicit_VR=True) - expected = [valuerep.DSfloat(x) for x in ["2", "4", "8", "16"]] got = ds.ROIContourSequence[0].ContourSequence[0].ContourData config.reset_data_element_callback() - assert expected == got + expected = [2., 4., 8., 16.] + if have_numpy and config.use_DS_numpy: + assert numpy.allclose(expected, got) + else: + assert expected == got diff -Nru pydicom-1.4.1/pydicom/tests/test_valuerep.py pydicom-2.0.0/pydicom/tests/test_valuerep.py --- pydicom-1.4.1/pydicom/tests/test_valuerep.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_valuerep.py 2020-05-29 01:44:31.000000000 +0000 @@ -3,35 +3,21 @@ """Test suite for valuerep.py""" import copy -from datetime import ( - datetime, - date, - time, - timedelta -) +from datetime import datetime, date, time, timedelta, timezone from pydicom.tag import Tag from pydicom.values import convert_value import pydicom import platform -from pydicom.compat import in_py2 from pydicom import config from pydicom import valuerep -from pydicom.util.fixes import timezone from pydicom.data import get_testdata_files from pydicom.valuerep import DS, IS import pytest -if not in_py2: - from pydicom.valuerep import PersonName3 as PersonNameUnicode +from pydicom.valuerep import PersonName - PersonName = PersonNameUnicode -else: - from pydicom.valuerep import ( - PersonName, - PersonNameUnicode - ) try: import cPickle as pickle @@ -39,19 +25,21 @@ import pickle badvr_name = get_testdata_files("badVR.dcm")[0] -default_encoding = 'iso8859' +default_encoding = "iso8859" -@pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason="PyPy has trouble with this pickle") -class TestTM(object): +@pytest.mark.skipif( + platform.python_implementation() == "PyPy", + reason="PyPy has trouble with this pickle", +) +class TestTM: """Unit tests for pickling TM""" def test_pickling(self): # Check that a pickled TM is read back properly x = pydicom.valuerep.TM("212223") - x.original_string = 'hello' - assert 'hello' == x.original_string + x.original_string = "hello" + assert "hello" == x.original_string assert time(21, 22, 23) == x data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) @@ -60,13 +48,13 @@ assert str(x) == str(x2) -class TestDT(object): +class TestDT: """Unit tests for pickling DT""" def test_pickling(self): # Check that a pickled DT is read back properly x = pydicom.valuerep.DT("19111213212123") - x.original_string = 'hello' + x.original_string = "hello" data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) assert x == x2 @@ -74,13 +62,13 @@ assert str(x) == str(x2) -class TestDA(object): +class TestDA: """Unit tests for pickling DA""" def test_pickling(self): # Check that a pickled DA is read back properly x = pydicom.valuerep.DA("19111213") - x.original_string = 'hello' + x.original_string = "hello" data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) assert x == x2 @@ -88,27 +76,29 @@ assert str(x) == str(x2) -class TestDS(object): +class TestDS: """Unit tests for DS values""" + def test_empty_value(self): assert DS(None) is None - assert '' == DS('') + assert "" == DS("") def test_float_values(self): val = DS(0.9) assert isinstance(val, pydicom.valuerep.DSfloat) assert 0.9 == val - val = DS('0.9') + val = DS("0.9") assert isinstance(val, pydicom.valuerep.DSfloat) assert 0.9 == val -class TestDSfloat(object): +class TestDSfloat: """Unit tests for pickling DSfloat""" + def test_pickling(self): # Check that a pickled DSFloat is read back properly x = pydicom.valuerep.DSfloat(9.0) - x.original_string = 'hello' + x.original_string = "hello" data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) assert x.real == x2.real @@ -117,28 +107,29 @@ def test_str(self): """Test DSfloat.__str__().""" val = pydicom.valuerep.DSfloat(1.1) - assert '1.1' == str(val) + assert "1.1" == str(val) - val = pydicom.valuerep.DSfloat('1.1') - assert '1.1' == str(val) + val = pydicom.valuerep.DSfloat("1.1") + assert "1.1" == str(val) def test_repr(self): """Test DSfloat.__repr__().""" val = pydicom.valuerep.DSfloat(1.1) assert '"1.1"' == repr(val) - val = pydicom.valuerep.DSfloat('1.1') + val = pydicom.valuerep.DSfloat("1.1") assert '"1.1"' == repr(val) -class TestDSdecimal(object): +class TestDSdecimal: """Unit tests for pickling DSdecimal""" + def test_pickling(self): # Check that a pickled DSdecimal is read back properly # DSdecimal actually prefers original_string when # reading back x = pydicom.valuerep.DSdecimal(19) - x.original_string = '19' + x.original_string = "19" data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) assert x.real == x2.real @@ -146,34 +137,36 @@ def test_float_value(self): config.allow_DS_float = False - with pytest.raises(TypeError, - match='cannot be instantiated with a float value'): + with pytest.raises( + TypeError, match="cannot be instantiated with a float value" + ): pydicom.valuerep.DSdecimal(9.0) config.allow_DS_float = True assert 9 == pydicom.valuerep.DSdecimal(9.0) -class TestIS(object): +class TestIS: """Unit tests for IS""" + def test_empty_value(self): assert IS(None) is None - assert '' == IS('') + assert "" == IS("") def test_valid_value(self): assert 42 == IS(42) - assert 42 == IS('42') + assert 42 == IS("42") assert 42 == IS(42.0) def test_invalid_value(self): - with pytest.raises(TypeError, match='Could not convert value'): + with pytest.raises(TypeError, match="Could not convert value"): IS(0.9) - with pytest.raises(ValueError, match='invalid literal for int()'): - IS('0.9') + with pytest.raises(ValueError, match="invalid literal for int()"): + IS("0.9") def test_pickling(self): # Check that a pickled IS is read back properly x = pydicom.valuerep.IS(921) - x.original_string = 'hello' + x.original_string = "hello" data1_string = pickle.dumps(x) x2 = pickle.loads(data1_string) assert x.real == x2.real @@ -197,30 +190,30 @@ def test_str(self): """Test IS.__str__().""" val = pydicom.valuerep.IS(1) - assert '1' == str(val) + assert "1" == str(val) - val = pydicom.valuerep.IS('1') - assert '1' == str(val) + val = pydicom.valuerep.IS("1") + assert "1" == str(val) def test_repr(self): """Test IS.__repr__().""" val = pydicom.valuerep.IS(1) assert '"1"' == repr(val) - val = pydicom.valuerep.IS('1') + val = pydicom.valuerep.IS("1") assert '"1"' == repr(val) -class TestBadValueRead(object): +class TestBadValueRead: """Unit tests for handling a bad value for a VR (a string in a number VR here)""" def setup(self): - class TagLike(object): + class TagLike: pass self.tag = TagLike() - self.tag.value = b'1A' + self.tag.value = b"1A" self.tag.is_little_endian = True self.tag.is_implicit_VR = False self.tag.tag = Tag(0x0010, 0x0020) @@ -232,24 +225,24 @@ def test_read_bad_value_in_VR_default(self): # found a conversion - assert '1A' == convert_value('SH', self.tag) + assert "1A" == convert_value("SH", self.tag) # converted with fallback vr "SH" - assert '1A' == convert_value('IS', self.tag) + assert "1A" == convert_value("IS", self.tag) - pydicom.values.convert_retry_VR_order = ['FL', 'UL'] + pydicom.values.convert_retry_VR_order = ["FL", "UL"] # no fallback VR succeeded, returned original value untranslated - assert b'1A' == convert_value('IS', self.tag) + assert b"1A" == convert_value("IS", self.tag) def test_read_bad_value_in_VR_enforce_valid_value(self): pydicom.config.enforce_valid_values = True # found a conversion - assert '1A' == convert_value('SH', self.tag) + assert "1A" == convert_value("SH", self.tag) # invalid literal for base 10 with pytest.raises(ValueError): - convert_value('IS', self.tag) + convert_value("IS", self.tag) -class TestDecimalString(object): +class TestDecimalString: """Unit tests unique to the use of DS class derived from python Decimal""" @@ -261,42 +254,48 @@ config.DS_decimal(False) config.enforce_valid_values = False + def test_DS_decimal_set(self): + config.use_DS_decimal = False + config.DS_decimal(True) + assert config.use_DS_decimal is True + def test_valid_decimal_strings(self): # Ensures that decimal.Decimal doesn't cause a valid string to become # invalid - valid_str = '-9.81338674e-006' + valid_str = "-9.81338674e-006" ds = valuerep.DS(valid_str) assert len(str(ds)) <= 16 # Now the input string is too long but decimal.Decimal can convert it # to a valid 16-character string - long_str = '-0.000000981338674' + long_str = "-0.000000981338674" ds = valuerep.DS(long_str) assert len(str(ds)) <= 16 def test_invalid_decimal_strings(self): # Now the input string truly is invalid - invalid_string = '-9.813386743e-006' + invalid_string = "-9.813386743e-006" with pytest.raises(OverflowError): valuerep.DS(invalid_string) -class TestPersonName(object): +class TestPersonName: def test_last_first(self): """PN: Simple Family-name^Given-name works...""" pn = PersonName("Family^Given") assert "Family" == pn.family_name - assert 'Given' == pn.given_name - assert '' == pn.name_suffix - assert '' == pn.phonetic + assert "Given" == pn.given_name + assert "" == pn.name_suffix + assert "" == pn.phonetic def test_copy(self): """PN: Copy and deepcopy works...""" - pn = PersonNameUnicode( - 'Hong^Gildong=' - '\033$)C\373\363^\033$)C\321\316\324\327=' - '\033$)C\310\253^\033$)C\261\346\265\277', - [default_encoding, 'euc_kr']) + pn = PersonName( + "Hong^Gildong=" + "\033$)C\373\363^\033$)C\321\316\324\327=" + "\033$)C\310\253^\033$)C\261\346\265\277", + [default_encoding, "euc_kr"], + ) pn_copy = copy.copy(pn) assert pn == pn_copy assert pn.components == pn_copy.components @@ -315,9 +314,11 @@ """PN: 3component (single-byte, ideographic, phonetic characters) works...""" # Example name from PS3.5-2008 section I.2 p. 108 - pn = PersonName('Hong^Gildong=' - '\033$)C\373\363^\033$)C\321\316\324\327=' - '\033$)C\310\253^\033$)C\261\346\265\277') + pn = PersonName( + "Hong^Gildong=" + "\033$)C\373\363^\033$)C\321\316\324\327=" + "\033$)C\310\253^\033$)C\261\346\265\277" + ) assert ("Hong", "Gildong") == (pn.family_name, pn.given_name) def test_formatting(self): @@ -328,97 +329,106 @@ def test_unicode_kr(self): """PN: 3component in unicode works (Korean)...""" # Example name from PS3.5-2008 section I.2 p. 101 - pn = PersonNameUnicode(b'Hong^Gildong=' - b'\033$)C\373\363^\033$)C\321\316\324\327=' - b'\033$)C\310\253^\033$)C\261\346\265\277', - [default_encoding, 'euc_kr']) - # PersonNameUnicode and PersonName3 behave differently: - # PersonName3 does not decode the components automatically - if not in_py2: - pn = pn.decode() - assert (u'Hong', u'Gildong') == (pn.family_name, pn.given_name) - assert u'洪^吉洞' == pn.ideographic - assert u'홍^길동' == pn.phonetic + pn = PersonName( + b"Hong^Gildong=" + b"\033$)C\373\363^\033$)C\321\316\324\327=" + b"\033$)C\310\253^\033$)C\261\346\265\277", + [default_encoding, "euc_kr"], + ) + + # PersonName does not decode the components automatically + pn = pn.decode() + assert (u"Hong", u"Gildong") == (pn.family_name, pn.given_name) + assert u"洪^吉洞" == pn.ideographic + assert u"홍^길동" == pn.phonetic def test_unicode_jp_from_bytes(self): """PN: 3component in unicode works (Japanese)...""" # Example name from PS3.5-2008 section H p. 98 - pn = PersonNameUnicode(b'Yamada^Tarou=' - b'\033$B;3ED\033(B^\033$BB@O:\033(B=' - b'\033$B$d$^$@\033(B^\033$B$?$m$&\033(B', - [default_encoding, 'iso2022_jp']) - if not in_py2: - pn = pn.decode() - assert (u'Yamada', u'Tarou') == (pn.family_name, pn.given_name) - assert u'山田^太郎' == pn.ideographic - assert u'やまだ^たろう' == pn.phonetic + pn = PersonName( + b"Yamada^Tarou=" + b"\033$B;3ED\033(B^\033$BB@O:\033(B=" + b"\033$B$d$^$@\033(B^\033$B$?$m$&\033(B", + [default_encoding, "iso2022_jp"], + ) + pn = pn.decode() + assert (u"Yamada", u"Tarou") == (pn.family_name, pn.given_name) + assert u"山田^太郎" == pn.ideographic + assert u"やまだ^たろう" == pn.phonetic def test_unicode_jp_from_bytes_comp_delimiter(self): """The example encoding without the escape sequence before '='""" - pn = PersonNameUnicode(b'Yamada^Tarou=' - b'\033$B;3ED\033(B^\033$BB@O:=' - b'\033$B$d$^$@\033(B^\033$B$?$m$&\033(B', - [default_encoding, 'iso2022_jp']) - if not in_py2: - pn = pn.decode() - assert (u'Yamada', u'Tarou') == (pn.family_name, pn.given_name) - assert u'山田^太郎' == pn.ideographic - assert u'やまだ^たろう' == pn.phonetic + pn = PersonName( + b"Yamada^Tarou=" + b"\033$B;3ED\033(B^\033$BB@O:=" + b"\033$B$d$^$@\033(B^\033$B$?$m$&\033(B", + [default_encoding, "iso2022_jp"], + ) + pn = pn.decode() + assert (u"Yamada", u"Tarou") == (pn.family_name, pn.given_name) + assert u"山田^太郎" == pn.ideographic + assert u"やまだ^たろう" == pn.phonetic def test_unicode_jp_from_bytes_caret_delimiter(self): """PN: 3component in unicode works (Japanese)...""" # Example name from PS3.5-2008 section H p. 98 - pn = PersonNameUnicode(b'Yamada^Tarou=' - b'\033$B;3ED\033(B^\033$BB@O:\033(B=' - b'\033$B$d$^$@\033(B^\033$B$?$m$&\033(B', - [default_encoding, 'iso2022_jp']) - if not in_py2: - pn = pn.decode() - assert (u'Yamada', u'Tarou') == (pn.family_name, pn.given_name) - assert u'山田^太郎' == pn.ideographic - assert u'やまだ^たろう' == pn.phonetic + pn = PersonName( + b"Yamada^Tarou=" + b"\033$B;3ED\033(B^\033$BB@O:\033(B=" + b"\033$B$d$^$@\033(B^\033$B$?$m$&\033(B", + [default_encoding, "iso2022_jp"], + ) + pn = pn.decode() + assert (u"Yamada", u"Tarou") == (pn.family_name, pn.given_name) + assert u"山田^太郎" == pn.ideographic + assert u"やまだ^たろう" == pn.phonetic def test_unicode_jp_from_unicode(self): """A person name initialized from unicode is already decoded""" - pn = PersonNameUnicode(u'Yamada^Tarou=山田^太郎=やまだ^たろう', - [default_encoding, 'iso2022_jp']) - assert (u'Yamada', u'Tarou') == (pn.family_name, pn.given_name) - assert u'山田^太郎' == pn.ideographic - assert u'やまだ^たろう' == pn.phonetic + pn = PersonName( + u"Yamada^Tarou=山田^太郎=やまだ^たろう", [default_encoding, "iso2022_jp"] + ) + assert (u"Yamada", u"Tarou") == (pn.family_name, pn.given_name) + assert u"山田^太郎" == pn.ideographic + assert u"やまだ^たろう" == pn.phonetic def test_not_equal(self): """PN3: Not equal works correctly (issue 121)...""" # Meant to only be used in python 3 but doing simple check here - from pydicom.valuerep import PersonName3 - pn = PersonName3("John^Doe") + from pydicom.valuerep import PersonName + + pn = PersonName("John^Doe") assert not pn != "John^Doe" def test_encoding_carried(self): """Test encoding is carried over to a new PN3 object""" # Issue 466 - from pydicom.valuerep import PersonName3 - pn = PersonName3("John^Doe", encodings='iso_ir_126') - assert pn.encodings == ('iso_ir_126',) - pn2 = PersonName3(pn) - assert pn2.encodings == ('iso_ir_126',) + from pydicom.valuerep import PersonName + + pn = PersonName("John^Doe", encodings="iso_ir_126") + assert pn.encodings == ("iso_ir_126",) + pn2 = PersonName(pn) + assert pn2.encodings == ("iso_ir_126",) def test_hash(self): """Test that the same name creates the same hash.""" - # Regression test for #785 in Python 3 - pn1 = PersonNameUnicode("John^Doe^^Dr", encodings=default_encoding) - pn2 = PersonNameUnicode("John^Doe^^Dr", encodings=default_encoding) + # Regression test for #785 + pn1 = PersonName("John^Doe^^Dr", encodings=default_encoding) + pn2 = PersonName("John^Doe^^Dr", encodings=default_encoding) assert hash(pn1) == hash(pn2) - pn3 = PersonNameUnicode("John^Doe", encodings=default_encoding) + pn3 = PersonName("John^Doe", encodings=default_encoding) assert hash(pn1) != hash(pn3) - pn1 = PersonNameUnicode(u'Yamada^Tarou=山田^太郎=やまだ^たろう', - [default_encoding, 'iso2022_jp']) - pn2 = PersonNameUnicode(u'Yamada^Tarou=山田^太郎=やまだ^たろう', - [default_encoding, 'iso2022_jp']) + pn1 = PersonName( + u"Yamada^Tarou=山田^太郎=やまだ^たろう", [default_encoding, "iso2022_jp"] + ) + pn2 = PersonName( + u"Yamada^Tarou=山田^太郎=やまだ^たろう", [default_encoding, "iso2022_jp"] + ) assert hash(pn1) == hash(pn2) -class TestDateTime(object): +class TestDateTime: """Unit tests for DA, DT, TM conversion to datetime objects""" def setup(self): @@ -439,7 +449,6 @@ dicom_date = "1961.08.04" # ACR-NEMA Standard 300 da = valuerep.DA(dicom_date) # Assert `da` equals to correct `date` - datetime_date = date(1961, 8, 4) assert date(1961, 8, 4) == da # Assert `da.__repr__` holds original string assert '"{0}"'.format(dicom_date) == repr(da) @@ -475,8 +484,9 @@ dicom_datetime = "196108041924-1000" dt = valuerep.DT(dicom_datetime) # Assert `dt` equals to correct `datetime` - datetime_datetime = datetime(1961, 8, 4, 19, 24, 0, 0, - timezone(timedelta(seconds=-10 * 3600))) + datetime_datetime = datetime( + 1961, 8, 4, 19, 24, 0, 0, timezone(timedelta(seconds=-10 * 3600)) + ) assert datetime_datetime == dt assert timedelta(0, 0, 0, 0, 0, -10) == dt.utcoffset() diff -Nru pydicom-1.4.1/pydicom/tests/test_values.py pydicom-2.0.0/pydicom/tests/test_values.py --- pydicom-1.4.1/pydicom/tests/test_values.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/tests/test_values.py 2020-05-29 01:44:31.000000000 +0000 @@ -5,12 +5,13 @@ import pytest from pydicom.tag import Tag -from pydicom.values import (convert_value, converters, convert_tag, - convert_ATvalue, convert_DA_string, convert_text, - convert_single_string, convert_AE_string) +from pydicom.values import ( + convert_value, converters, convert_tag, convert_ATvalue, convert_DA_string, + convert_text, convert_single_string, convert_AE_string +) -class TestConvertTag(object): +class TestConvertTag: def test_big_endian(self): """Test convert_tag with a big endian byte string""" bytestring = b'\x00\x10\x00\x20' @@ -40,13 +41,13 @@ convert_tag(bytestring, True) -class TestConvertAE(object): +class TestConvertAE: def test_strip_blanks(self): bytestring = b' AE_TITLE ' assert u'AE_TITLE' == convert_AE_string(bytestring, True) -class TestConvertText(object): +class TestConvertText: def test_single_value(self): """Test that encoding can change inside a text string""" bytestring = (b'Dionysios is \x1b\x2d\x46' @@ -102,7 +103,7 @@ assert ['Values', 'with zeros'] == convert_text(bytestring) -class TestConvertAT(object): +class TestConvertAT: def test_big_endian(self): """Test convert_ATvalue with a big endian byte string""" # VM 1 @@ -144,7 +145,7 @@ convert_ATvalue(bytestring, True) -class TestConvertDA(object): +class TestConvertDA: def test_big_endian(self): """Test convert_DA_string with a big endian byte string""" # VM 1 @@ -175,7 +176,7 @@ assert convert_DA_string(bytestring, True) == '' -class TestConvertValue(object): +class TestConvertValue: def test_convert_value_raises(self): """Test convert_value raises exception if unsupported VR""" converter_func = converters['PN'] @@ -188,3 +189,11 @@ # Fix converters converters['PN'] = converter_func assert 'PN' in converters + + +class TestConvertOValues: + """Test converting values with the 'O' VRs like OB, OW, OF, etc.""" + def test_convert_of(self): + """Test converting OF.""" + fp = b'\x00\x01\x02\x03' + assert b'\x00\x01\x02\x03' == converters['OF'](fp, True) diff -Nru pydicom-1.4.1/pydicom/uid.py pydicom-2.0.0/pydicom/uid.py --- pydicom-1.4.1/pydicom/uid.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/uid.py 2020-05-29 01:44:31.000000000 +0000 @@ -8,7 +8,6 @@ import re from pydicom._uid_dict import UID_dictionary -from pydicom import compat # Many thanks to the Medical Connections for offering free # valid UIDs (http://www.medicalconnections.co.uk/FreeUID.html) @@ -65,7 +64,7 @@ if isinstance(val, UID): return val - if isinstance(val, compat.string_types): + if isinstance(val, str): return super(UID, cls).__new__(cls, val.strip()) raise TypeError("UID must be a string") diff -Nru pydicom-1.4.1/pydicom/util/codify.py pydicom-2.0.0/pydicom/util/codify.py --- pydicom-1.4.1/pydicom/util/codify.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/util/codify.py 2020-05-29 01:44:31.000000000 +0000 @@ -18,7 +18,6 @@ import os.path import pydicom from pydicom.datadict import dictionary_keyword -from pydicom.compat import int_type import re @@ -71,12 +70,10 @@ :return: a string of import statement lines """ - line0 = "from __future__ import unicode_literals" - line0 += " # Only for python2.7 and save_as unicode filename" line1 = "import pydicom" - line2 = "from pydicom.dataset import Dataset" + line2 = "from pydicom.dataset import Dataset, FileMetaDataset" line3 = "from pydicom.sequence import Sequence" - return line_term.join((line0, line1, line2, line3)) + return line_term.join((line1, line2, line3)) def code_dataelem(dataelem, @@ -202,7 +199,8 @@ def code_dataset(ds, dataset_name="ds", exclude_size=None, - include_private=False): + include_private=False, + is_file_meta=False): """Return python code lines for import statements needed by other code :arg exclude_size: if specified, values longer than this (in bytes) @@ -215,7 +213,8 @@ """ lines = [] - lines.append(dataset_name + " = Dataset()") + ds_class = " = FileMetaDataset()" if is_file_meta else " = Dataset()" + lines.append(dataset_name + ds_class) for dataelem in ds: # If a private data element and flag says so, skip it and go to next if not include_private and dataelem.tag.is_private: @@ -262,7 +261,7 @@ # Code the file_meta information lines.append("# File meta info data elements") code_meta = code_dataset(ds.file_meta, "file_meta", exclude_size, - include_private) + include_private, is_file_meta=True) lines.append(code_meta) lines.append('') @@ -323,7 +322,7 @@ parser.add_argument( '-e', '--exclude-size', - type=int_type, + type=int, default=default_exclude_size, help=help_exclude_size) parser.add_argument( diff -Nru pydicom-1.4.1/pydicom/util/dump.py pydicom-2.0.0/pydicom/util/dump.py --- pydicom-1.4.1/pydicom/util/dump.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/util/dump.py 2020-05-29 01:44:31.000000000 +0000 @@ -1,7 +1,6 @@ # Copyright 2008-2018 pydicom authors. See LICENSE file for details. """Utility functions used in debugging writing and reading""" -from __future__ import print_function from io import BytesIO diff -Nru pydicom-1.4.1/pydicom/util/fixes.py pydicom-2.0.0/pydicom/util/fixes.py --- pydicom-1.4.1/pydicom/util/fixes.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/util/fixes.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,135 +0,0 @@ -# Copyright 2008-2018 pydicom authors. See LICENSE file for details. -"""Compatibility fixes for older version of python.""" - -import sys - -from datetime import datetime, tzinfo, timedelta - -if sys.version_info[0] < 3: - class timezone(tzinfo): - """Backport of datetime.timezone. - - Notes - ----- - Backport of datetime.timezone for Python 2.7, from Python 3.6 - documentation (https://tinyurl.com/z4cegu9), copyright Python Software - Foundation (https://docs.python.org/3/license.html) - - """ - __slots__ = '_offset', '_name' - - # Sentinel value to disallow None - _Omitted = object() - - def __new__(cls, offset, name=_Omitted): - if not isinstance(offset, timedelta): - raise TypeError("offset must be a timedelta") - if name is cls._Omitted: - if not offset: - return cls.utc - name = None - elif not isinstance(name, str): - raise TypeError("name must be a string") - if not cls._minoffset <= offset <= cls._maxoffset: - raise ValueError("offset must be a timedelta " - "strictly between -timedelta(hours=24) and " - "timedelta(hours=24).") - if (offset.microseconds != 0 or offset.seconds % 60 != 0): - raise ValueError("offset must be a timedelta " - "representing a whole number of minutes") - return cls._create(offset, name) - - @classmethod - def _create(cls, offset, name=None): - self = tzinfo.__new__(cls) - self._offset = offset - self._name = name - return self - - def __getinitargs__(self): - """pickle support""" - if self._name is None: - return (self._offset,) - return (self._offset, self._name) - - def __eq__(self, other): - if type(other) != timezone: - return False - return self._offset == other._offset - - def __lt__(self, other): - raise TypeError("'<' not supported between instances of" - " 'datetime.timezone' and 'datetime.timezone'") - - def __hash__(self): - return hash(self._offset) - - def __repr__(self): - if self is self.utc: - return '%s.%s.utc' % (self.__class__.__module__, - self.__class__.__name__) - if self._name is None: - return "%s.%s(%r)" % (self.__class__.__module__, - self.__class__.__name__, - self._offset) - return "%s.%s(%r, %r)" % (self.__class__.__module__, - self.__class__.__name__, - self._offset, self._name) - - def __str__(self): - return self.tzname(None) - - def utcoffset(self, dt): - if isinstance(dt, datetime) or dt is None: - return self._offset - raise TypeError("utcoffset() argument must be a datetime instance" - " or None") - - def tzname(self, dt): - if isinstance(dt, datetime) or dt is None: - if self._name is None: - return self._name_from_offset(self._offset) - return self._name - raise TypeError("tzname() argument must be a datetime instance" - " or None") - - def dst(self, dt): - if isinstance(dt, datetime) or dt is None: - return None - raise TypeError("dst() argument must be a datetime instance" - " or None") - - def fromutc(self, dt): - if isinstance(dt, datetime): - if dt.tzinfo is not self: - raise ValueError("fromutc: dt.tzinfo " - "is not self") - return dt + self._offset - raise TypeError("fromutc() argument must be a datetime instance" - " or None") - - _maxoffset = timedelta(hours=23, minutes=59) - _minoffset = -_maxoffset - - @staticmethod - def _name_from_offset(delta): - if not delta: - return 'UTC' - if delta < timedelta(0): - sign = '-' - delta = -delta - else: - sign = '+' - hours, rest = divmod(delta.total_seconds(), 3600) - hours = int(hours) - minutes = rest // timedelta(minutes=1).total_seconds() - minutes = int(minutes) - return 'UTC{}{:02d}:{:02d}'.format(sign, hours, minutes) - - timezone.utc = timezone._create(timedelta(0)) - timezone.min = timezone._create(timezone._minoffset) - timezone.max = timezone._create(timezone._maxoffset) - _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) - -else: - from datetime import timezone diff -Nru pydicom-1.4.1/pydicom/util/hexutil.py pydicom-2.0.0/pydicom/util/hexutil.py --- pydicom-1.4.1/pydicom/util/hexutil.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/util/hexutil.py 2020-05-29 01:44:31.000000000 +0000 @@ -3,8 +3,6 @@ from binascii import (a2b_hex, b2a_hex) -from pydicom import compat -from pydicom.compat import in_py2 from pydicom.charset import default_encoding @@ -34,13 +32,12 @@ # true in 2.x so the difference in bytes constructor doesn't matter if isinstance(hexstring, bytes): return a2b_hex(hexstring.replace(b" ", b"")) - elif isinstance(hexstring, compat.string_types): + elif isinstance(hexstring, str): return a2b_hex(bytes(hexstring.replace(" ", ""), default_encoding)) raise TypeError('argument shall be bytes or string type') def bytes2hex(byte_string): s = b2a_hex(byte_string) - if not in_py2: - s = s.decode() + s = s.decode() return " ".join(s[i:i + 2] for i in range(0, len(s), 2)) diff -Nru pydicom-1.4.1/pydicom/util/leanread.py pydicom-2.0.0/pydicom/util/leanread.py --- pydicom-1.4.1/pydicom/util/leanread.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/util/leanread.py 2020-05-29 01:44:31.000000000 +0000 @@ -15,7 +15,7 @@ SequenceDelimiterTag = 0xFFFEE0DD # end of Sequence of undefined length -class dicomfile(object): +class dicomfile: """Context-manager based DICOM file object with data element iteration""" def __init__(self, filename): diff -Nru pydicom-1.4.1/pydicom/valuerep.py pydicom-2.0.0/pydicom/valuerep.py --- pydicom-1.4.1/pydicom/valuerep.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/valuerep.py 2020-05-29 01:44:31.000000000 +0000 @@ -4,13 +4,11 @@ from decimal import Decimal import re -from datetime import (date, datetime, time, timedelta) +from datetime import (date, datetime, time, timedelta, timezone) # don't import datetime_conversion directly from pydicom import config -from pydicom import compat from pydicom.multival import MultiValue -from pydicom.util.fixes import timezone # can't import from charset or get circular import default_encoding = "iso8859" @@ -28,20 +26,19 @@ # Delimiters for text strings and person name that reset the encoding. # See PS3.5, Section 6.1.2.5.3 -# Note: We use characters for Python 2 and character codes for Python 3 -# because these are the types yielded if iterating over a byte string. +# Note: We use character codes for Python 3 +# because those are the types yielded if iterating over a byte string. # Characters/Character codes for text VR delimiters: LF, CR, TAB, FF -TEXT_VR_DELIMS = ({'\n', '\r', '\t', '\f'} if compat.in_py2 - else {0x0d, 0x0a, 0x09, 0x0c}) +TEXT_VR_DELIMS = {0x0d, 0x0a, 0x09, 0x0c} # Character/Character code for PN delimiter: name part separator '^' # (the component separator '=' is handled separately) -PN_DELIMS = {'^'} if compat.in_py2 else {0xe5} +PN_DELIMS = {0xe5} class DA(date): - """Store value for an element with VR 'DA' as :class:`datetime.date`. + """Store value for an element with VR **DA** as :class:`datetime.date`. Note that the :class:`datetime.date` base class is immutable. """ @@ -73,7 +70,7 @@ A string conformant to the DA definition in the DICOM Standard, Part 5, :dcm:`Table 6.2-1`. """ - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): if len(val) == 8: year = int(val[0:4]) month = int(val[4:6]) @@ -101,7 +98,7 @@ return val def __init__(self, val): - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): self.original_string = val elif isinstance(val, DA) and hasattr(val, 'original_string'): self.original_string = val.original_string @@ -117,7 +114,7 @@ class DT(datetime): - """Store value for an element with VR 'DT' as :class:`datetime.datetime`. + """Store value for an element with VR **DT** as :class:`datetime.datetime`. Note that the :class:`datetime.datetime` base class is immutable. """ @@ -154,7 +151,7 @@ A string conformant to the DT definition in the DICOM Standard, Part 5, :dcm:`Table 6.2-1`. """ - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): match = DT._regex_dt.match(val) if match and len(val) <= 26: dt_match = match.group(2) @@ -213,7 +210,7 @@ return val def __init__(self, val): - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): self.original_string = val elif isinstance(val, DT) and hasattr(val, 'original_string'): self.original_string = val.original_string @@ -229,7 +226,7 @@ class TM(time): - """Store value for an element with VR 'TM' as :class:`datetime.time`. + """Store value for an element with VR **TM** as :class:`datetime.time`. Note that the :class:`datetime.time` base class is immutable. """ @@ -262,7 +259,7 @@ A string conformant to the TM definition in the DICOM Standard, Part 5, :dcm:`Table 6.2-1`. """ - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): match = TM._regex_tm.match(val) if match and len(val) <= 16: tm_match = match.group(1) @@ -298,7 +295,7 @@ return val def __init__(self, val): - if isinstance(val, (str, compat.string_types)): + if isinstance(val, str): self.original_string = val elif isinstance(val, TM) and hasattr(val, 'original_string'): self.original_string = val.original_string @@ -314,7 +311,7 @@ class DSfloat(float): - """Store value for an element with VR 'DS' as :class:`float`. + """Store value for an element with VR **DS** as :class:`float`. If constructed from an empty string, return the empty string, not an instance of this class. @@ -338,7 +335,7 @@ # a different object, because float is immutable. has_attribute = hasattr(val, 'original_string') - if isinstance(val, (str, compat.text_type)): + if isinstance(val, str): self.original_string = val elif isinstance(val, (DSfloat, DSdecimal)) and has_attribute: self.original_string = val.original_string @@ -355,7 +352,7 @@ class DSdecimal(Decimal): - """Store value for an element with VR 'DS' as :class:`decimal.Decimal`. + """Store value for an element with VR **DS** as :class:`decimal.Decimal`. Notes ----- @@ -387,7 +384,7 @@ enforce_length = config.enforce_valid_values # DICOM allows spaces around the string, # but python doesn't, so clean it - if isinstance(val, (str, compat.text_type)): + if isinstance(val, str): val = val.strip() # If the input string is actually invalid that we relax the valid # value constraint for this particular instance @@ -420,7 +417,7 @@ """ # ... also if user changes a data element value, then will get # a different Decimal, as Decimal is immutable. - if isinstance(val, (str, compat.text_type)): + if isinstance(val, str): self.original_string = val elif isinstance(val, (DSfloat, DSdecimal)) and hasattr(val, 'original_string'): # noqa self.original_string = val.original_string @@ -453,7 +450,7 @@ Similarly the string clean and check can be avoided and :class:`DSfloat` called directly if a string has already been processed. """ - if isinstance(val, (str, compat.text_type)): + if isinstance(val, str): val = val.strip() if val == '' or val is None: return val @@ -461,40 +458,21 @@ class IS(int): - """Store value for an element with VR 'IS' as :class:`int`. + """Store value for an element with VR **IS** as :class:`int`. Stores original integer string for exact rewriting of the string originally read or stored. """ - if compat.in_py2: - __slots__ = ['original_string'] - - # Unlikely that str(int) will not be the - # same as the original, but could happen - # with leading zeros. - - def __getstate__(self): - return dict((slot, getattr(self, slot)) for slot in self.__slots__ - if hasattr(self, slot)) - - def __setstate__(self, state): - for slot, value in state.items(): - setattr(self, slot, value) def __new__(cls, val): """Create instance if new integer string""" if val is None: return val - if isinstance(val, (str, compat.text_type)) and val.strip() == '': + if isinstance(val, str) and val.strip() == '': return '' - # Overflow error in Python 2 for integers too large - # while calling super(IS). Fall back on the regular int - # casting that will automatically convert the val to long - # if needed. - try: - newval = super(IS, cls).__new__(cls, val) - except OverflowError: - newval = int(val) + + newval = super(IS, cls).__new__(cls, val) + # check if a float or Decimal passed in, then could have lost info, # and will raise error. E.g. IS(Decimal('1')) is ok, but not IS(1.23) if isinstance(val, (float, Decimal)) and newval != val: @@ -509,7 +487,7 @@ def __init__(self, val): # If a string passed, then store it - if isinstance(val, (str, compat.text_type)): + if isinstance(val, str): self.original_string = val elif isinstance(val, IS) and hasattr(val, 'original_string'): self.original_string = val.original_string @@ -585,7 +563,7 @@ """ from pydicom.charset import decode_string - if isinstance(components[0], compat.text_type): + if isinstance(components[0], str): comps = components else: comps = [decode_string(comp, encodings, PN_DELIMS) @@ -628,15 +606,15 @@ return b'='.join(encoded_comps) -class PersonName3(object): +class PersonName: def __new__(cls, *args, **kwargs): - # Handle None value by returning None instead of a PersonName3 object + # Handle None value by returning None instead of a PersonName object if len(args) and args[0] is None: return None - return super(PersonName3, cls).__new__(cls) + return super(PersonName, cls).__new__(cls) def __init__(self, val, encodings=None, original_string=None): - if isinstance(val, PersonName3): + if isinstance(val, PersonName): encodings = val.encodings self.original_string = val.original_string self._components = tuple(str(val).split('=')) @@ -789,7 +767,7 @@ Returns ------- - valuerep.PersonName3 + valuerep.PersonName A person name object that will return the decoded string with the given encodings on demand. If the encodings are not given, the current object is returned. @@ -804,7 +782,7 @@ # if the original encoding was not set, we set it now self.original_string = _encode_personname( self.components, self.encodings or [default_encoding]) - return PersonName3(self.original_string, encodings) + return PersonName(self.original_string, encodings) def encode(self, encodings=None): """Return the patient name decoded by the given `encodings`. @@ -849,151 +827,5 @@ return bool(self.original_string) -class PersonNameBase(object): - """Base class for Person Name classes""" - - def __init__(self, val): - """Initialize the PN properties""" - # Note normally use __new__ on subclassing an immutable, - # but here we just want to do some pre-processing - # for properties PS 3.5-2008 section 6.2 (p.28) - # and 6.2.1 describes PN. Briefly: - # single-byte-characters=ideographic - # characters=phonetic-characters - # (each with?): - # family-name-complex - # ^Given-name-complex - # ^Middle-name^name-prefix^name-suffix - self.parse() - - def formatted(self, format_str): - """Return a formatted string according to the format pattern - - Parameters - ---------- - format_str : str - The string to use for formatting the PN element value. Use - "...%(property)...%(property)..." where property is one of - `family_name`, `given_name`, `middle_name`, `name_prefix`, or - `name_suffix`. - - Returns - ------- - str - The formatted PN element value. - """ - return format_str % self.__dict__ - - def parse(self): - """Break down the components and name parts""" - self.components = tuple(self.split("=")) - nComponents = len(self.components) - self.single_byte = self.components[0] - self.ideographic = '' - self.phonetic = '' - if nComponents > 1: - self.ideographic = self.components[1] - if nComponents > 2: - self.phonetic = self.components[2] - - if self.single_byte: - # in case missing trailing items are left out - name_string = self.single_byte + "^^^^" - parts = name_string.split("^")[:5] - self.family_name, self.given_name, self.middle_name = parts[:3] - self.name_prefix, self.name_suffix = parts[3:] - else: - (self.family_name, self.given_name, self.middle_name, - self.name_prefix, self.name_suffix) = ('', '', '', '', '') - - -class PersonName(PersonNameBase, bytes): - """Human-friendly class to hold the value of elements with VR of 'PN'. - - The value is parsed into the following properties: - - * single-byte, ideographic, and phonetic components (DICOM Standard, Part - 5, :dcm:`Section 6.2.1`) - * family_name, given_name, middle_name, name_prefix, name_suffix - """ - - def __new__(cls, val): - """Return instance of the new class""" - # Check if trying to convert a string that has already been converted - if isinstance(val, PersonName): - return val - return super(PersonName, cls).__new__(cls, val) - - def encode(self, *args): - """Dummy method to mimic py2 str behavior in py3 bytes subclass""" - # This greatly simplifies the write process so all objects have the - # "encode" method - return self - - def family_comma_given(self): - """Return name as 'Family-name, Given-name'""" - return self.formatted("%(family_name)s, %(given_name)s") - - # def __str__(self): - # return str(self.byte_string) - # XXX need to process the ideographic or phonetic components? - # def __len__(self): - # return len(self.byte_string) - - -class PersonNameUnicode(PersonNameBase, compat.text_type): - """Unicode version of Person Name""" - - def __new__(cls, val, encodings): - """Return unicode string after conversion of each part - - Parameters - ---------- - val : bytes or str - The PN value to store - encodings : list of str - A list of python encodings, generally found from - ``pydicom.charset.python_encodings`` mapping of values in DICOM - data element (0008,0005) *Specific Character Set*. - """ - encodings = _verify_encodings(encodings) - comps = _decode_personname(val.split(b"="), encodings) - new_val = u"=".join(comps) - return compat.text_type.__new__(cls, new_val) - - def __init__(self, val, encodings): - self.encodings = _verify_encodings(encodings) - PersonNameBase.__init__(self, val) - - def __copy__(self): - """Correctly copy object. - - Needed because of the overwritten __new__. - """ - # no need to use the original encoding here - we just encode and - # decode in utf-8 and set the original encoding later - name = compat.text_type(self).encode('utf8') - new_person = PersonNameUnicode(name, 'utf8') - new_person.__dict__.update(self.__dict__) - return new_person - - def __deepcopy__(self, memo): - """Make correctly a deep copy of the object. - - Needed because of the overwritten __new__. - """ - name = compat.text_type(self).encode('utf8') - new_person = PersonNameUnicode(name, 'utf8') - memo[id(self)] = new_person - # no need for deepcopy call - all attributes are immutable - new_person.__dict__.update(self.__dict__) - return new_person - - def encode(self, encodings): - """Encode the unicode using the specified encoding""" - encodings = _verify_encodings(encodings) or self.encodings - return _encode_personname(self.split('='), encodings) - - def family_comma_given(self): - """Return name as 'Family-name, Given-name'""" - return self.formatted("%(family_name)u, %(given_name)u") +# Alias old class names for backwards compat in user code +PersonNameUnicode = PersonName = PersonName diff -Nru pydicom-1.4.1/pydicom/values.py pydicom-2.0.0/pydicom/values.py --- pydicom-1.4.1/pydicom/values.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/values.py 2020-05-29 01:44:31.000000000 +0000 @@ -3,15 +3,14 @@ data elements to proper python types """ +import re from io import BytesIO from struct import (unpack, calcsize) # don't import datetime_conversion directly from pydicom import config -from pydicom import compat -from pydicom.compat import in_py2 from pydicom.charset import (default_encoding, text_VRs, decode_string) -from pydicom.config import logger +from pydicom.config import logger, have_numpy from pydicom.dataelem import empty_value_for_VR from pydicom.filereader import read_sequence from pydicom.multival import MultiValue @@ -20,10 +19,14 @@ import pydicom.valuerep # don't import DS directly as can be changed by config from pydicom.valuerep import (MultiString, DA, DT, TM, TEXT_VR_DELIMS) -if not in_py2: - from pydicom.valuerep import PersonName3 as PersonName -else: - from pydicom.valuerep import PersonNameUnicode as PersonName + +have_numpy = True +try: + import numpy +except ImportError: + have_numpy = False + +from pydicom.valuerep import PersonName # NOQA def convert_tag(byte_string, is_little_endian, offset=0): @@ -70,8 +73,7 @@ str The decoded 'AE' value without non-significant spaces. """ - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) byte_string = byte_string.strip() return byte_string @@ -132,8 +134,7 @@ otherwise returns :class:`str` or ``list`` of ``str``. """ if config.datetime_conversion: - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) splitup = byte_string.split("\\") if len(splitup) == 1: return _DA_from_byte_string(splitup[0]) @@ -146,6 +147,10 @@ def convert_DS_string(byte_string, is_little_endian, struct_format=None): """Return a decoded 'DS' value. + .. versionchanged:: 2.0 + + The option to return numpy values was added. + Parameters ---------- byte_string : bytes or str @@ -157,19 +162,45 @@ Returns ------- - valuerep.DSfloat or valuerep.DSdecimal or list of DSfloat/DSdecimal - If :attr:`~pydicom.config.use_DS_decimal` is ``True`` then returns - :class:`~pydicom.valuerep.DSdecimal` or a :class:`list` of - ``DSdecimal``, otherwise returns :class:`~pydicom.valuerep.DSfloat` or - a ``list`` of ``DSfloat``. + :class:`~pydicom.valuerep.DSfloat`, :class:`~pydicom.valuerep.DSdecimal`, :class:`numpy.float64`, list of DSfloat/DSdecimal or :class:`numpy.ndarray` of :class:`numpy.float64` + + If :attr:`~pydicom.config.use_DS_decimal` is ``False`` (default), + returns a :class:`~pydicom.valuerep.DSfloat` or list of them + + If :attr:`~pydicom.config.use_DS_decimal` is ``True``, + returns a :class:`~pydicom.valuerep.DSdecimal` or list of them + + If :data:`~pydicom.config.use_DS_numpy` is ``True``, + returns a :class:`numpy.float64` or a :class:`numpy.ndarray` of them + + Raises + ------ + ValueError + If :data:`~pydicom.config.use_DS_numpy` is ``True`` and the string + contains non-valid characters + + ImportError + If :data:`~pydicom.config.use_DS_numpy` is ``True`` and numpy is not + available """ - if not in_py2: - byte_string = byte_string.decode(default_encoding) + num_string = byte_string.decode(default_encoding) # Below, go directly to DS class instance # rather than factory DS, but need to # ensure last string doesn't have # blank padding (use strip()) - return MultiString(byte_string.strip(), valtype=pydicom.valuerep.DSclass) + if config.use_DS_numpy: + if not have_numpy: + raise ImportError("use_DS_numpy set but numpy not installed") + # Check for valid characters. Numpy ignores many + regex = r'[ \\0-9\.+eE-]*\Z' + if re.match(regex, num_string) is None: + raise ValueError("DS: char(s) not in repertoire: '{}'". + format(re.sub(regex[:-2], '', num_string))) + value = numpy.fromstring(num_string, dtype='f8', sep="\\") + if len(value) == 1: # Don't use array for one number + value = value[0] + return value + return MultiString(num_string.strip(), valtype=pydicom.valuerep.DSclass) def _DT_from_byte_string(byte_string): @@ -195,14 +226,13 @@ Returns ------- str or list of str or valuerep.DT or list of DT - if + If :attr:`~pydicom.config.datetime_conversion` is ``True`` then returns :class:`~pydicom.valuerep.DT` or a :class:`list` of ``DT``, otherwise returns :class:`str` or ``list`` of ``str``. """ if config.datetime_conversion: - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) splitup = byte_string.split("\\") if len(splitup) == 1: return _DT_from_byte_string(splitup[0]) @@ -215,6 +245,10 @@ def convert_IS_string(byte_string, is_little_endian, struct_format=None): """Return a decoded 'IS' value. + .. versionchanged:: 2.0 + + The option to return numpy values was added. + Parameters ---------- byte_string : bytes or str @@ -226,12 +260,40 @@ Returns ------- - valuerep.IS or list of IS - The decoded value(s). - """ - if not in_py2: - byte_string = byte_string.decode(default_encoding) - return MultiString(byte_string, valtype=pydicom.valuerep.IS) + :class:`~pydicom.valuerep.IS` or list of them, or :class:`numpy.int64` or :class:`~numpy.ndarray` of them + + If :data:`~pydicom.config.use_IS_numpy` is ``False`` (default), returns + a single :class:`~pydicom.valuerep.IS` or a list of them + + If :data:`~pydicom.config.use_IS_numpy` is ``True``, returns + a single :class:`numpy.int64` or a :class:`~numpy.ndarray` of them + + Raises + ------ + ValueError + If :data:`~pydicom.config.use_IS_numpy` is ``True`` and the string + contains non-valid characters + + ImportError + If :data:`~pydicom.config.use_IS_numpy` is ``True`` and numpy is not + available + """ + num_string = byte_string.decode(default_encoding) + + if config.use_IS_numpy: + if not have_numpy: + raise ImportError("use_IS_numpy set but numpy not installed") + # Check for valid characters. Numpy ignores many + regex = r'[ \\0-9\.+-]*\Z' + if re.match(regex, num_string) is None: + raise ValueError("IS: char(s) not in repertoire: '{}'". + format(re.sub(regex[:-2], '', num_string))) + value = numpy.fromstring(num_string, dtype='i8', sep=chr(92)) # 92:'\' + if len(value) == 1: # Don't use array for one number + value = value[0] + return value + + return MultiString(num_string, valtype=pydicom.valuerep.IS) def convert_numbers(byte_string, is_little_endian, struct_format): @@ -322,19 +384,12 @@ Returns ------- - valuerep.PersonName3 or list of PersonName3 - The decoded 'PN' value(s) if using Python 3. - valuerep.PersonNameUnicode or list of PersonNameUnicode - The decoded 'PN' value(s) if using Python 2. + valuerep.PersonName or list of PersonName + The decoded 'PN' value(s). """ def get_valtype(x): - if not in_py2: - return PersonName(x, encodings).decode() - return PersonName(x, encodings) - - # XXX - We have to replicate MultiString functionality - # here because we can't decode easily here since that - # is performed in PersonNameUnicode + return PersonName(x, encodings).decode() + if byte_string.endswith((b' ', b'\x00')): byte_string = byte_string[:-1] @@ -366,8 +421,7 @@ str or list of str The decoded value(s). """ - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) return MultiString(byte_string) @@ -385,17 +439,15 @@ Returns ------- - unicode or list of unicode - The decoded value(s) if in Python 2. str or list of str - The decoded value(s) if in Python 3. + The decoded value(s). """ values = byte_string.split(b'\\') values = [convert_single_string(value, encodings) for value in values] if len(values) == 1: return values[0] else: - return MultiValue(compat.text_type, values) + return MultiValue(str, values) def convert_single_string(byte_string, encodings=None): @@ -410,10 +462,8 @@ Returns ------- - unicode or list of unicode - The decoded text if in Python 2. str or list of str - The decoded text if in Python 3. + The decoded text. """ encodings = encodings or [default_encoding] value = decode_string(byte_string, encodings, TEXT_VR_DELIMS) @@ -480,8 +530,7 @@ otherwise returns :class:`str` or ``list`` of ``str``. """ if config.datetime_conversion: - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) splitup = byte_string.split("\\") if len(splitup) == 1: return _TM_from_byte_string(splitup[0]) @@ -511,8 +560,7 @@ The decoded 'UI' element value without a trailing null. """ # Strip off 0-byte padding for even length (if there) - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) if byte_string and byte_string.endswith('\0'): byte_string = byte_string[:-1] return MultiString(byte_string, pydicom.uid.UID) @@ -543,8 +591,7 @@ bytes or str The encoded 'UR' element value without any trailing spaces. """ - if not in_py2: - byte_string = byte_string.decode(default_encoding) + byte_string = byte_string.decode(default_encoding) byte_string = byte_string.rstrip() return byte_string @@ -565,6 +612,7 @@ type or list of type The element value decoded using the appropriate decoder. """ + if VR not in converters: # `VR` characters are in the ascii alphabet ranges 65 - 90, 97 - 122 char_range = list(range(65, 91)) + list(range(97, 123)) @@ -588,7 +636,7 @@ # Ensure that encodings is a list encodings = encodings or [default_encoding] - if isinstance(encodings, compat.string_types): + if isinstance(encodings, str): encodings = [encodings] byte_string = raw_data_element.value @@ -659,7 +707,7 @@ 'LT': convert_single_string, 'OB': convert_OBvalue, 'OD': convert_OBvalue, - 'OF': (convert_numbers, 'f'), + 'OF': convert_OWvalue, 'OL': convert_OBvalue, 'OW': convert_OWvalue, 'OV': convert_OVvalue, diff -Nru pydicom-1.4.1/pydicom/_version.py pydicom-2.0.0/pydicom/_version.py --- pydicom-1.4.1/pydicom/_version.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/pydicom/_version.py 2020-05-29 01:44:31.000000000 +0000 @@ -2,6 +2,6 @@ import re -__version__ = '1.4.1' +__version__ = '2.0.0' __version_info__ = tuple( re.match(r'(\d+\.\d+\.\d+).*', __version__).group(1).split('.')) diff -Nru pydicom-1.4.1/README.md pydicom-2.0.0/README.md --- pydicom-1.4.1/README.md 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/README.md 2020-05-29 01:44:31.000000000 +0000 @@ -7,7 +7,7 @@ [![codecov](https://codecov.io/gh/pydicom/pydicom/branch/master/graph/badge.svg)](https://codecov.io/gh/pydicom/pydicom) [![Python version](https://img.shields.io/pypi/pyversions/pydicom.svg)](https://img.shields.io/pypi/pyversions/pydicom.svg) [![PyPI version](https://badge.fury.io/py/pydicom.svg)](https://badge.fury.io/py/pydicom) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3333768.svg)](https://doi.org/10.5281/zenodo.3333768) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3614067.svg)](https://doi.org/10.5281/zenodo.3614067) [![Gitter](https://badges.gitter.im/pydicom/Lobby.svg)](https://gitter.im/pydicom/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) *pydicom* is a pure python package for working with [DICOM](http://medical.nema.org/) files. @@ -36,7 +36,7 @@ documentation for [the previous 0.9.9 version](https://pydicom.github.io/pydicom/0.9/) is still there for reference. -See [Getting Started](https://pydicom.github.io/pydicom/stable/getting_started.html) +See [Getting Started](https://pydicom.github.io/pydicom/stable/old/getting_started.html) for installation and basic information, and the [User Guide](https://pydicom.github.io/pydicom/stable/pydicom_user_guide.html) for an overview of how to use the *pydicom* library. diff -Nru pydicom-1.4.1/setup.cfg pydicom-2.0.0/setup.cfg --- pydicom-1.4.1/setup.cfg 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/setup.cfg 2020-05-29 01:44:31.000000000 +0000 @@ -2,4 +2,4 @@ test=pytest [bdist_wheel] -universal=1 +universal=0 diff -Nru pydicom-1.4.1/setup.py pydicom-2.0.0/setup.py --- pydicom-1.4.1/setup.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/setup.py 2020-05-29 01:44:31.000000000 +0000 @@ -23,8 +23,7 @@ needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) pytest_runner = ['pytest-runner'] if needs_pytest else [] -# in_py2 check in next line - pytest>=5 requires Python 3 -TESTS_REQUIRE = ['pytest<5'] if sys.version_info[0] == 2 else ['pytest'] +TESTS_REQUIRE = ['pytest'] _py_modules = [] if not have_dicom: _py_modules = ['dicom'] @@ -36,7 +35,6 @@ "Intended Audience :: Science/Research", "Development Status :: 5 - Production/Stable", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -81,6 +79,7 @@ PACKAGE_DATA = {'pydicom': data_files_inventory()} opts = dict(name=NAME, + python_requires='>=3.5', version=VERSION, maintainer=MAINTAINER, maintainer_email=MAINTAINER_EMAIL, diff -Nru pydicom-1.4.1/source/generate_dict/generate_dicom_dict.py pydicom-2.0.0/source/generate_dict/generate_dicom_dict.py --- pydicom-1.4.1/source/generate_dict/generate_dicom_dict.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/source/generate_dict/generate_dicom_dict.py 2020-05-29 01:44:31.000000000 +0000 @@ -46,18 +46,13 @@ import os import xml.etree.ElementTree as ET - -try: - import urllib2 - # python2 - -except ImportError: - import urllib.request as urllib2 - # python3 +import urllib.request as urllib2 _DIRECTORY = os.path.dirname(__file__) -PYDICOM_DICT_FILENAME = os.path.join(_DIRECTORY, '../../pydicom/_dicom_dict.py') +PYDICOM_DICT_FILENAME = os.path.join( + _DIRECTORY, '../../pydicom/_dicom_dict.py' +) MAIN_DICT_NAME = 'DicomDictionary' MASK_DICT_NAME = 'RepeatersDictionary' @@ -299,7 +294,6 @@ FILE_DOCSTRING = '"""DICOM data dictionary auto-generated by %s"""\n' \ % os.path.basename(__file__) py_file.write(FILE_DOCSTRING) -py_file.write('from __future__ import absolute_import\n\n') py_file.write('# Each dict entry is Tag : (VR, VM, Name, Retired, Keyword)') write_dict(py_file, MAIN_DICT_NAME, main_attributes, tag_is_string=False) diff -Nru pydicom-1.4.1/source/generate_uids/generate_storage_sopclass_uids.py pydicom-2.0.0/source/generate_uids/generate_storage_sopclass_uids.py --- pydicom-1.4.1/source/generate_uids/generate_storage_sopclass_uids.py 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/source/generate_uids/generate_storage_sopclass_uids.py 2020-05-29 01:44:31.000000000 +0000 @@ -6,10 +6,7 @@ import re import tokenize -from pydicom.compat import in_py2 - import pydicom - from pydicom._uid_dict import UID_dictionary STORAGE_REGEX = re.compile('.*(Storage|Storage SOP Class|Storage - ' @@ -65,14 +62,8 @@ def replace_bad_characters(name): bad_chars = r'!@#$%^&*(),;:.?\|{}[]+-=/ ' - if not in_py2: - translate_table = dict((ord(char), None) for char in bad_chars) - name = name.translate(translate_table) - else: - import string - translate_table = string.maketrans('', '') - name = name.translate(translate_table, bad_chars) - return name + translate_table = dict((ord(char), None) for char in bad_chars) + return name.translate(translate_table) def uid_line(uid, name): @@ -89,7 +80,6 @@ '"""\nStorage SOP Class UIDs auto-generated by %s\n"""\n\n' % os.path.basename(__file__)) uid_file.write(doc_string) - uid_file.write('from __future__ import absolute_import\n') uid_file.write('from pydicom.uid import UID\n\n') for uid, attribs in sorted(UID_dictionary.items()): diff -Nru pydicom-1.4.1/.travis.yml pydicom-2.0.0/.travis.yml --- pydicom-1.4.1/.travis.yml 2020-01-20 21:48:33.000000000 +0000 +++ pydicom-2.0.0/.travis.yml 2020-05-29 01:44:31.000000000 +0000 @@ -11,18 +11,9 @@ matrix: include: - - env: DISTRIB="pypy" PYTHON_VERSION="2.7" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false - - env: DISTRIB="pypy" PYTHON_VERSION="2.7" NUMPY=true PILLOW=false JPEG_LS=false GDCM=false - env: DISTRIB="pypy" PYTHON_VERSION="3.5" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false - env: DISTRIB="pypy" PYTHON_VERSION="3.5" NUMPY=true PILLOW=false JPEG_LS=false GDCM=false - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=false JPEG_LS=false GDCM=false - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=false JPEG_LS=false GDCM=true - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=both JPEG_LS=false GDCM=false - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=both JPEG_LS=true GDCM=false - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=both JPEG_LS=true GDCM=true - - env: DISTRIB="conda" PYTHON_VERSION="3.5" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false - env: DISTRIB="conda" PYTHON_VERSION="3.5" NUMPY=true PILLOW=false JPEG_LS=false GDCM=false - env: DISTRIB="conda" PYTHON_VERSION="3.5" NUMPY=true PILLOW=false JPEG_LS=false GDCM=true @@ -52,12 +43,8 @@ - env: DISTRIB="conda" PYTHON_VERSION="3.8" NUMPY=true PILLOW=both JPEG_LS=true GDCM=false # - env: DISTRIB="conda" PYTHON_VERSION="3.8" NUMPY=true PILLOW=both JPEG_LS=true GDCM=true - - env: DISTRIB="conda" PYTHON_VERSION="2.7" NUMPY=true PILLOW=jpeg JPEG_LS=false GDCM=false - env: DISTRIB="conda" PYTHON_VERSION="3.7.3" NUMPY=true PILLOW=jpeg JPEG_LS=false GDCM=false - - env: DISTRIB="ubuntu" PYTHON_VERSION="2.7" NUMPY=false PILLOW=false JPEG_LS=false GDCM=false - - env: DISTRIB="ubuntu" PYTHON_VERSION="2.7" NUMPY=true PILLOW=false JPEG_LS=false GDCM=false - install: source build_tools/travis/install.sh script: