[Git][debian-gis-team/pystac][upstream] New upstream version 1.12.2

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sat Feb 22 17:21:31 GMT 2025



Antonio Valentino pushed to branch upstream at Debian GIS Project / pystac


Commits:
768dc38c by Antonio Valentino at 2025-02-22T16:35:09+00:00
New upstream version 1.12.2
- - - - -


25 changed files:

- .github/pull_request_template.md
- .github/workflows/continuous-integration.yml
- .gitignore
- + .python-version
- .readthedocs.yaml
- CHANGELOG.md
- README.md
- docs/contributing.rst
- − docs/pyproject.toml
- pyproject.toml
- pystac/serialization/identify.py
- pystac/version.py
- − scripts/bench
- scripts/pull-static
- − scripts/test
- tests/cassettes/test_item/ItemTest.test_null_geometry.yaml → tests/cassettes/test_item/test_null_geometry.yaml
- tests/conftest.py
- tests/extensions/test_eo.py
- tests/serialization/test_identify.py
- tests/test_collection.py
- tests/test_item.py
- tests/test_item_assets.py
- tests/test_item_collection.py
- tests/test_version.py
- uv.lock


Changes:

=====================================
.github/pull_request_template.md
=====================================
@@ -6,7 +6,8 @@
 
 **PR Checklist:**
 
-- [ ] Pre-commit hooks and tests pass (run `scripts/test`)
+- [ ] Pre-commit hooks pass (run `pre-commit run --all-files`)
+- [ ] Tests pass (run `pytest`)
 - [ ] Documentation has been updated to reflect changes, if applicable
 - [ ] This PR maintains or improves overall codebase code coverage.
 - [ ] Changes are added to the [CHANGELOG](https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md). See [the docs](https://pystac.readthedocs.io/en/latest/contributing.html#changelog) for information about adding to the changelog.


=====================================
.github/workflows/continuous-integration.yml
=====================================
@@ -13,7 +13,6 @@ on:
   merge_group:
 
 concurrency:
-  # Cancel running job if another commit is pushed to the branch
   group: ${{ github.ref }}
   cancel-in-progress: true
 
@@ -34,14 +33,14 @@ jobs:
           - macos-latest
     steps:
       - uses: actions/checkout at v4
-      - uses: actions/setup-python at v5
-        with:
-          python-version: ${{ matrix.python-version }}
       - uses: astral-sh/setup-uv at v5
         with:
-          enable-cache: true
+          python-version: ${{ matrix.python-version }}
       - name: Sync
         run: uv sync --all-extras
+      - name: Lint
+        if: runner.os != 'Windows'
+        run: uv run pre-commit run --all-files
       - name: Test on windows
         if: runner.os == 'Windows'
         shell: bash
@@ -50,19 +49,14 @@ jobs:
         run: uv run pytest tests
       - name: Test
         if: runner.os != 'Windows'
-        run: uv run scripts/test
+        run: uv run pytest tests --block-network --record-mode=none
 
   coverage:
     name: coverage
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout at v4
-      - uses: actions/setup-python at v5
-        with:
-          python-version: "3.10"
       - uses: astral-sh/setup-uv at v5
-        with:
-          enable-cache: true
       - name: Install with dependencies
         run: uv sync --all-extras
       - name: Run coverage with orjson
@@ -80,45 +74,18 @@ jobs:
         if: ${{ env.GITHUB_REPOSITORY }} == 'stac-utils/pystac'
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
-          file: ./coverage.xml
+          files: ./coverage.xml
           fail_ci_if_error: false
       - name: Check for coverage drop
         # This will use the configured fail-under, causing this job to fail if the
         # coverage drops.
         run: uv run coverage report
 
-  lint:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version:
-          - "3.10"
-          - "3.11"
-          - "3.12"
-          - "3.13"
-    steps:
-      - uses: actions/checkout at v4
-      - uses: actions/setup-python at v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - uses: astral-sh/setup-uv at v5
-        with:
-          enable-cache: true
-      - name: Sync
-        run: uv sync
-      - name: Execute linters & type checkers
-        run: uv run pre-commit run --all-files
-
   without-orjson:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout at v4
-      - uses: actions/setup-python at v5
-        with:
-          python-version: "3.10"
       - uses: astral-sh/setup-uv at v5
-        with:
-          enable-cache: true
       - name: Sync
         run: uv sync
       - name: Uninstall orjson
@@ -150,15 +117,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout at v4
-      - uses: actions/setup-python at v5
-        with:
-          python-version: "3.10"
       - uses: astral-sh/setup-uv at v5
-        with:
-          enable-cache: true
       - name: Install pandoc
         run: sudo apt-get install pandoc
-      - name: Install pystac
-        run: uv sync --no-dev && uv sync --package pystac-docs --inexact
+      - name: Sync
+        run: uv sync --group docs
       - name: Check docs
         run: uv run make -C docs html SPHINXOPTS="-W --keep-going -n"


=====================================
.gitignore
=====================================
@@ -107,7 +107,7 @@ ipython_config.py
 # pyenv
 #   For a library or package, you might want to ignore these files since the code is
 #   intended to run in multiple environments; otherwise, check them in:
-.python-version
+# .python-version
 
 # pipenv
 #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.


=====================================
.python-version
=====================================
@@ -0,0 +1 @@
+3.10
\ No newline at end of file


=====================================
.readthedocs.yaml
=====================================
@@ -14,8 +14,7 @@ build:
     - asdf plugin add uv
     - asdf install uv latest
     - asdf global uv latest
-    - uv sync --all-extras --no-dev
-    - uv sync --package pystac-docs --inexact # we need inexact to keep the top-level sync packages 
+    - uv sync --group docs
     - uv run sphinx-build -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html
 
 formats:


=====================================
CHANGELOG.md
=====================================
@@ -2,6 +2,12 @@
 
 ## [Unreleased]
 
+## [v1.12.2]
+
+### Fixed
+
+- Make sure that `VersionRange` has `VersionID`s rather than strings ([#1512](https://github.com/stac-utils/pystac/pull/1512))
+
 ## [v1.12.1]
 
 ### Changed
@@ -904,7 +910,8 @@ use `Band.create`
 
 Initial release.
 
-[Unreleased]: <https://github.com/stac-utils/pystac/compare/v1.12.1..main>
+[Unreleased]: <https://github.com/stac-utils/pystac/compare/v1.12.2..main>
+[v1.12.2]: <https://github.com/stac-utils/pystac/compare/v1.12.1..v1.12.2>
 [v1.12.1]: <https://github.com/stac-utils/pystac/compare/v1.12.0..v1.12.1>
 [v1.12.0]: <https://github.com/stac-utils/pystac/compare/v1.11.0..v1.12.0>
 [v1.11.0]: <https://github.com/stac-utils/pystac/compare/v1.10.1..v1.11.0>


=====================================
README.md
=====================================
@@ -1,6 +1,6 @@
 # PySTAC
 
-[![Build Status](https://github.com/stac-utils/pystac/workflows/CI/badge.svg?branch=main)](https://github.com/stac-utils/pystac/actions/workflows/continuous-integration.yml)
+[![Build Status](https://github.com/stac-utils/pystac/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/stac-utils/pystac/actions/workflows/continuous-integration.yml)
 [![PyPI version](https://badge.fury.io/py/pystac.svg)](https://badge.fury.io/py/pystac)
 [![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/pystac)](https://anaconda.org/conda-forge/pystac)
 [![Documentation](https://readthedocs.org/projects/pystac/badge/?version=latest)](https://pystac.readthedocs.io/en/latest/)


=====================================
docs/contributing.rst
=====================================
@@ -33,7 +33,7 @@ To run the tests and generate the coverage report:
 
 .. code-block:: bash
 
-    $ pytest -v -s --block-network --cov pystac --cov-report term-missing
+    $ pytest -v -s --cov pystac --cov-report term-missing
 
 To view the coverage report, you can run
 `coverage report` (to view the report in the terminal) or `coverage html` (to generate
@@ -100,7 +100,7 @@ and report any improvements or regressions.
 
 .. code-block:: bash
 
-    scripts/bench
+    asv continuous --split -e --interleave-rounds --factor 1.25 main HEAD
 
 The benchmark suite takes a while to run, and will report any significant
 changes to standard output. For example, here's a benchmark comparison between


=====================================
docs/pyproject.toml deleted
=====================================
@@ -1,19 +0,0 @@
-[project]
-name = "pystac-docs"
-version = "0.0.0"
-description = "This package is only for uv, so it can share a lockfile with the top-level project. It should never be published"
-requires-python = ">=3.10"
-dependencies = [
-    "boto3>=1.35.39",
-    "ipython>=8.28.0",
-    "jinja2>=3.1.4",
-    "jupyter>=1.1.1",
-    "nbsphinx>=0.9.5",
-    "pydata-sphinx-theme>=0.15.4",
-    "rasterio>=1.4.1",
-    "shapely>=2.0.6",
-    "sphinx>=8.1.1",
-    "sphinx-autobuild>=2024.10.3",
-    "sphinx-design>=0.6.1",
-    "sphinxcontrib-fulltoc>=1.2.0",
-]


=====================================
pyproject.toml
=====================================
@@ -30,6 +30,47 @@ orjson = ["orjson>=3.5"]
 urllib3 = ["urllib3>=1.26"]
 validation = ["jsonschema~=4.18"]
 
+[dependency-groups]
+dev = [
+    "asv>=0.6.4",
+    "codespell<2.3",
+    "coverage>=7.6.2",
+    "doc8>=1.1.2",
+    "html5lib>=1.1",
+    "jinja2>=3.1.4",
+    "jsonschema>=4.23.0",
+    "mypy>=1.11.2",
+    "orjson>=3.10.7",
+    "packaging>=24.1",
+    "pre-commit>=4.0.1",
+    "pytest>=8.3.3",
+    "pytest-cov>=5.0.0",
+    "pytest-mock>=3.14.0",
+    "pytest-recording>=0.13.2",
+    "requests-mock>=1.12.1",
+    "ruff>=0.6.9",
+    "types-html5lib>=1.1.11.20240806",
+    "types-jsonschema>=4.23.0.20240813",
+    "types-orjson>=3.6.2",
+    "types-python-dateutil>=2.9.0.20241003",
+    "types-urllib3>=1.26.25.14",
+    "virtualenv>=20.26.6",
+]
+docs = [
+    "boto3>=1.35.39",
+    "ipython>=8.28.0",
+    "jinja2>=3.1.4",
+    "jupyter>=1.1.1",
+    "nbsphinx>=0.9.5",
+    "pydata-sphinx-theme>=0.15.4",
+    "rasterio>=1.4.1",
+    "shapely>=2.0.6",
+    "sphinx>=8.1.1",
+    "sphinx-autobuild>=2024.10.3",
+    "sphinx-design>=0.6.1",
+    "sphinxcontrib-fulltoc>=1.2.0",
+]
+
 [project.urls]
 Documentation = "https://pystac.readthedocs.io"
 Repository = "https://github.com/stac-utils/pystac"
@@ -64,6 +105,7 @@ lint.select = ["E", "F", "I"]
 
 [tool.pytest.ini_options]
 filterwarnings = ["error"]
+addopts = "--block-network --record-mode=none"
 
 [tool.setuptools.packages.find]
 include = ["pystac*"]
@@ -72,36 +114,6 @@ exclude = ["tests*", "benchmarks*"]
 [tool.setuptools.dynamic]
 version = { attr = "pystac.version.__version__" }
 
-[tool.uv]
-dev-dependencies = [
-    "asv>=0.6.4",
-    "codespell<2.3",
-    "coverage>=7.6.2",
-    "doc8>=1.1.2",
-    "html5lib>=1.1",
-    "jinja2>=3.1.4",
-    "jsonschema>=4.23.0",
-    "mypy>=1.11.2",
-    "orjson>=3.10.7",
-    "packaging>=24.1",
-    "pre-commit>=4.0.1",
-    "pytest>=8.3.3",
-    "pytest-cov>=5.0.0",
-    "pytest-mock>=3.14.0",
-    "pytest-recording>=0.13.2",
-    "requests-mock>=1.12.1",
-    "ruff>=0.6.9",
-    "types-html5lib>=1.1.11.20240806",
-    "types-jsonschema>=4.23.0.20240813",
-    "types-orjson>=3.6.2",
-    "types-python-dateutil>=2.9.0.20241003",
-    "types-urllib3>=1.26.25.14",
-    "virtualenv>=20.26.6",
-]
-
-[tool.uv.workspace]
-members = ["docs"]
-
 [build-system]
 requires = ["setuptools>=61.0"]
 build-backend = "setuptools.build_meta"


=====================================
pystac/serialization/identify.py
=====================================
@@ -82,8 +82,8 @@ class STACVersionID:
 class STACVersionRange:
     """Defines a range of STAC versions."""
 
-    min_version: STACVersionID
-    max_version: STACVersionID
+    _min_version: STACVersionID
+    _max_version: STACVersionID
 
     def __init__(
         self,
@@ -103,21 +103,45 @@ class STACVersionRange:
             else:
                 self.max_version = max_version
 
-    def set_min(self, v: STACVersionID) -> None:
+    @property
+    def min_version(self) -> STACVersionID:
+        return self._min_version
+
+    @min_version.setter
+    def min_version(self, v: str | STACVersionID) -> None:
+        if isinstance(v, str):
+            v = STACVersionID(v)
+        self._min_version = v
+
+    @property
+    def max_version(self) -> STACVersionID:
+        return self._max_version
+
+    @max_version.setter
+    def max_version(self, v: str | STACVersionID) -> None:
+        if isinstance(v, str):
+            v = STACVersionID(v)
+        self._max_version = v
+
+    def set_min(self, v: str | STACVersionID) -> None:
+        if isinstance(v, str):
+            v = STACVersionID(v)
         if self.min_version < v:
             if v < self.max_version:
                 self.min_version = v
             else:
                 self.min_version = self.max_version
 
-    def set_max(self, v: STACVersionID) -> None:
+    def set_max(self, v: str | STACVersionID) -> None:
+        if isinstance(v, str):
+            v = STACVersionID(v)
         if v < self.max_version:
             if self.min_version < v:
                 self.max_version = v
             else:
                 self.max_version = self.min_version
 
-    def set_to_single(self, v: STACVersionID) -> None:
+    def set_to_single(self, v: str | STACVersionID) -> None:
         self.set_min(v)
         self.set_max(v)
 
@@ -263,12 +287,12 @@ def identify_stac_object(json_dict: dict[str, Any]) -> STACJSONDescription:
     stac_extensions = json_dict.get("stac_extensions", None)
 
     if stac_version is None:
-        version_range.set_min(STACVersionID("0.8.0"))
+        version_range.set_min("0.8.0")
     else:
         version_range.set_to_single(stac_version)
 
     if stac_extensions is not None:
-        version_range.set_min(STACVersionID("0.8.0"))
+        version_range.set_min("0.8.0")
 
     if stac_extensions is None:
         stac_extensions = []


=====================================
pystac/version.py
=====================================
@@ -1,6 +1,6 @@
 import os
 
-__version__ = "1.12.1"
+__version__ = "1.12.2"
 """Library version"""
 
 


=====================================
scripts/bench deleted
=====================================
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-set -e
-
-if [[ -z $ASV_FACTOR ]]; then
-    ASV_FACTOR=1.25;
-fi
-
-asv continuous --split -e --interleave-rounds \
-    --factor ${ASV_FACTOR} \
-    main HEAD;


=====================================
scripts/pull-static
=====================================
@@ -2,7 +2,7 @@
 
 set -e
 
-VERSION="1.5.0" #v1.5.0-beta.2 should work or no??
+VERSION="1.5.0"
 SRC="https://cdn.jsdelivr.net/npm/@radiantearth/stac-fields@$VERSION/fields-normalized.json"
 HERE=$(dirname "$0")
 DEST=$(dirname "$HERE")/pystac/static/fields-normalized.json


=====================================
scripts/test deleted
=====================================
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-set -e
-
-if [[ -z ${CI} ]]; then
-    pre-commit run --all-files
-fi
-echo
-
-if [[ -z ${CI} || -n ${CHECK_COVERAGE} ]]; then
-    echo " -- RUNNING UNIT TESTS (WITH COVERAGE) --"
-    pytest tests --block-network --record-mode=none --cov
-else
-    echo " -- RUNNING UNIT TESTS (WITHOUT COVERAGE) --"
-    pytest tests --block-network --record-mode=none
-fi
-
-echo


=====================================
tests/cassettes/test_item/ItemTest.test_null_geometry.yaml → tests/cassettes/test_item/test_null_geometry.yaml
=====================================
@@ -7,7 +7,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/item.json
   response:
@@ -99,7 +99,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:39 GMT
       ETag:
       - '"66e1651c-147c"'
       Last-Modified:
@@ -111,19 +111,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - eee882ae55a7f40729d77f3d500a84bed4c637d0
+      - 9a54b2c2e8d8dfda667d69d0dd28acd3c223b4c7
       X-GitHub-Request-Id:
-      - 7F28:34A10D:FF6968:11D3377:67925A75
+      - 6B5F:16F8:3EC0E8:470F4B:679E5948
       X-Served-By:
-      - cache-den-kden1300051-DEN
+      - cache-bos4633-BOS
       X-Timer:
-      - S1737644661.145511,VS0,VE59
+      - S1738454440.623801,VS0,VE20
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-proxy-cache:
       - MISS
     status:
@@ -137,7 +137,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/basics.json
   response:
@@ -166,7 +166,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:39 GMT
       ETag:
       - '"66e1651c-21c"'
       Last-Modified:
@@ -178,19 +178,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - 24282bdf85a8fde571d18d0a3007d5cf6bf30fcb
+      - 8f6c3b198c4c026542ea5cc5b46485cfce14313c
       X-GitHub-Request-Id:
-      - 5ED4:12DF28:F44C6C:1121586:67925A75
+      - 7D47:4AAAC:67C965:716773:679E5948
       X-Served-By:
-      - cache-den-kden1300073-DEN
+      - cache-bos4643-BOS
       X-Timer:
-      - S1737644661.228637,VS0,VE68
+      - S1738454440.712740,VS0,VE30
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-origin-cache:
       - HIT
       x-proxy-cache:
@@ -206,7 +206,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/datetime.json
   response:
@@ -265,7 +265,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:39 GMT
       ETag:
       - '"66e1651c-a82"'
       Last-Modified:
@@ -277,19 +277,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - a29e86e905de4ad9406e730b8f75d8d9fc0e363d
+      - 530a4436422927e3d78506b58f78887510928f7f
       X-GitHub-Request-Id:
-      - 5488:230B9F:B893E:CFA99:67925A75
+      - 97C0:A1EF3:5E972D:68288C:679E5949
       X-Served-By:
-      - cache-den-kden1300042-DEN
+      - cache-bos4655-BOS
       X-Timer:
-      - S1737644661.327324,VS0,VE57
+      - S1738454440.811790,VS0,VE30
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-proxy-cache:
       - MISS
     status:
@@ -303,7 +303,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/instrument.json
   response:
@@ -334,7 +334,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:39 GMT
       ETag:
       - '"66e1651c-2a2"'
       Last-Modified:
@@ -346,21 +346,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - dd7f2955fbdd7a5b0b24cac94bac85eadaea1608
+      - efcbb9e72539f153ef7cf2b7377c273d12c96c9e
       X-GitHub-Request-Id:
-      - 802D:338B06:10BB8A9:1298392:67925A75
+      - FA0A:15A6D:5C46DD:65DCD4:679E5947
       X-Served-By:
-      - cache-den-kden1300065-DEN
+      - cache-bos4692-BOS
       X-Timer:
-      - S1737644661.410596,VS0,VE58
+      - S1738454440.905646,VS0,VE41
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
-      x-origin-cache:
-      - HIT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-proxy-cache:
       - MISS
     status:
@@ -374,7 +372,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/licensing.json
   response:
@@ -400,7 +398,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:40 GMT
       ETag:
       - '"66e1651c-135"'
       Last-Modified:
@@ -412,19 +410,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - 2b8fd8778031ec89e594e7017d5111a507d66dfc
+      - 3d703898d330148032f27df7d9d46c65c819285e
       X-GitHub-Request-Id:
-      - D377:3405BB:10C72BA:12A3CD2:67925A75
+      - 085E:1C811A:56FD1C:609622:679E5949
       X-Served-By:
-      - cache-den-kden1300032-DEN
+      - cache-bos4624-BOS
       X-Timer:
-      - S1737644661.495593,VS0,VE54
+      - S1738454440.007807,VS0,VE27
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-origin-cache:
       - HIT
       x-proxy-cache:
@@ -440,7 +438,7 @@ interactions:
       Host:
       - schemas.stacspec.org
       User-Agent:
-      - Python-urllib/3.12
+      - Python-urllib/3.13
     method: GET
     uri: https://schemas.stacspec.org/v1.0.0-beta.2/item-spec/json-schema/provider.json
   response:
@@ -476,7 +474,7 @@ interactions:
       Content-Type:
       - application/json; charset=utf-8
       Date:
-      - Thu, 23 Jan 2025 15:04:21 GMT
+      - Sun, 02 Feb 2025 00:00:40 GMT
       ETag:
       - '"66e1651c-40e"'
       Last-Modified:
@@ -488,19 +486,19 @@ interactions:
       Via:
       - 1.1 varnish
       X-Cache:
-      - MISS
+      - HIT
       X-Cache-Hits:
       - '0'
       X-Fastly-Request-ID:
-      - 84d8905f4abd1401e3d76535680a6bda88fa0568
+      - 01a1b60753cc5e73cb47bd5d87093ead8c9e73d8
       X-GitHub-Request-Id:
-      - 538F:1011A9:1037FFE:1214BBF:67925A75
+      - 6370:26E82C:5B4B43:64E477:679E5947
       X-Served-By:
-      - cache-den-kden1300062-DEN
+      - cache-bos4647-BOS
       X-Timer:
-      - S1737644662.570305,VS0,VE54
+      - S1738454440.107510,VS0,VE29
       expires:
-      - Thu, 23 Jan 2025 15:14:21 GMT
+      - Sat, 01 Feb 2025 17:36:33 GMT
       x-origin-cache:
       - HIT
       x-proxy-cache:


=====================================
tests/conftest.py
=====================================
@@ -1,9 +1,11 @@
 # TODO move all test case code to this file
 
+import json
 import shutil
 import uuid
 from datetime import datetime
 from pathlib import Path
+from typing import Any
 
 import pytest
 
@@ -61,6 +63,14 @@ def get_data_file(rel_path: str) -> str:
     return str(here / "data-files" / rel_path)
 
 
+ at pytest.fixture
+def sample_item_dict() -> dict[str, Any]:
+    m = TestCases.get_path("data-files/item/sample-item.json")
+    with open(m) as f:
+        item_dict: dict[str, Any] = json.load(f)
+    return item_dict
+
+
 @pytest.fixture
 def sample_item() -> Item:
     return Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))


=====================================
tests/extensions/test_eo.py
=====================================
@@ -516,3 +516,18 @@ def test_required_property_missing(ext_item: pystac.Item) -> None:
     assert bands is not None
     with pytest.raises(RequiredPropertyMissing):
         bands[0].name
+
+
+def test_unnecessary_migrations_not_performed(ext_item: Item) -> None:
+    item_as_dict = ext_item.to_dict(include_self_link=False, transform_hrefs=False)
+    item_as_dict["stac_version"] = "1.0.0"
+    item_as_dict["properties"]["eo:bands"] = [{"name": "B1", "common_name": "coastal"}]
+
+    item = Item.from_dict(item_as_dict)
+
+    migrated_item = pystac.Item.from_dict(item_as_dict, migrate=True)
+
+    assert item.properties == migrated_item.properties
+    assert len(item.assets) == len(migrated_item.assets)
+    for key, value in item.assets.items():
+        assert value.to_dict() == migrated_item.assets[key].to_dict()


=====================================
tests/serialization/test_identify.py
=====================================
@@ -118,3 +118,17 @@ class VersionTest(unittest.TestCase):
 
         version_range = STACVersionRange(min_version="0.6.0-rc1", max_version="0.9.0")
         self.assertTrue(version_range.contains("0.9.0"))
+
+    def test_version_range_set_to_single(self) -> None:
+        version_range = STACVersionRange()
+        version_range.set_min("1.0.0-beta.1")
+        version_range.set_to_single("1.0.0")
+
+        self.assertTrue(version_range.contains("1.0.0"))
+
+    def test_version_range_set_min_and_max_directly(self) -> None:
+        version_range = STACVersionRange()
+        version_range.min_version = "1.0.0-beta.1"  # type:ignore
+        version_range.max_version = "1.1.0"  # type:ignore
+
+        self.assertTrue(version_range.contains("1.0.0"))


=====================================
tests/test_collection.py
=====================================
@@ -3,7 +3,6 @@ from __future__ import annotations
 import json
 import os
 import tempfile
-import unittest
 from collections.abc import Iterator
 from copy import deepcopy
 from datetime import datetime
@@ -32,499 +31,512 @@ from tests.utils import ARBITRARY_BBOX, ARBITRARY_GEOM, TestCases
 TEST_DATETIME = datetime(2020, 3, 14, 16, 32)
 
 
-class ProviderTest(unittest.TestCase):
-    def test_to_from_dict(self) -> None:
-        provider_dict = {
-            "name": "Remote Data, Inc",
-            "description": "Producers of awesome spatiotemporal assets",
-            "roles": ["producer", "processor"],
-            "url": "http://remotedata.io",
-            "extension:field": "some value",
-        }
-        expected_extra_fields = {"extension:field": provider_dict["extension:field"]}
-
-        provider = Provider.from_dict(provider_dict)
-
-        self.assertEqual(provider_dict["name"], provider.name)
-        self.assertEqual(provider_dict["description"], provider.description)
-        self.assertEqual(provider_dict["roles"], provider.roles)
-        self.assertEqual(provider_dict["url"], provider.url)
-        self.assertDictEqual(expected_extra_fields, provider.extra_fields)
-
-        self.assertDictEqual(provider_dict, provider.to_dict())
-
-
-class CollectionTest(unittest.TestCase):
-    def test_spatial_extent_from_coordinates(self) -> None:
-        extent = SpatialExtent.from_coordinates(ARBITRARY_GEOM["coordinates"])
-
-        self.assertEqual(len(extent.bboxes), 1)
-        bbox = extent.bboxes[0]
-        self.assertEqual(len(bbox), 4)
-        for x in bbox:
-            self.assertTrue(isinstance(x, float))
-
-    def test_read_eo_items_are_heritable(self) -> None:
-        cat = TestCases.case_5()
-        item = next(cat.get_items(recursive=True))
-
-        self.assertTrue(EOExtension.has_extension(item))
-
-    def test_save_uses_previous_catalog_type(self) -> None:
-        collection = TestCases.case_8()
-        assert collection.STAC_OBJECT_TYPE == pystac.STACObjectType.COLLECTION
-        self.assertEqual(collection.catalog_type, CatalogType.SELF_CONTAINED)
-        with tempfile.TemporaryDirectory() as tmp_dir:
-            collection.normalize_hrefs(tmp_dir)
-            href = collection.self_href
-            collection.save()
-
-            collection2 = pystac.Collection.from_file(href)
-            self.assertEqual(collection2.catalog_type, CatalogType.SELF_CONTAINED)
-
-    def test_clone_uses_previous_catalog_type(self) -> None:
-        catalog = TestCases.case_8()
-        assert catalog.catalog_type == CatalogType.SELF_CONTAINED
-        clone = catalog.clone()
-        self.assertEqual(clone.catalog_type, CatalogType.SELF_CONTAINED)
-
-    def test_clone_cant_mutate_original(self) -> None:
-        collection = TestCases.case_8()
-        assert collection.keywords is not None
-        self.assertListEqual(collection.keywords, ["disaster", "open"])
-        clone = collection.clone()
-        clone.extra_fields["test"] = "extra"
-        self.assertNotIn("test", collection.extra_fields)
-        assert clone.keywords is not None
-        clone.keywords.append("clone")
-        self.assertListEqual(clone.keywords, ["disaster", "open", "clone"])
-        self.assertListEqual(collection.keywords, ["disaster", "open"])
-        self.assertNotEqual(id(collection.summaries), id(clone.summaries))
-
-    def test_multiple_extents(self) -> None:
-        cat1 = TestCases.case_1()
-        country = cat1.get_child("country-1")
-        assert country is not None
-        col1 = country.get_child("area-1-1")
-        assert col1 is not None
-        col1.validate()
-        self.assertIsInstance(col1, Collection)
-        validate_dict(col1.to_dict(), pystac.STACObjectType.COLLECTION)
-
-        multi_ext_uri = TestCases.get_path("data-files/collections/multi-extent.json")
-        with open(multi_ext_uri) as f:
-            multi_ext_dict = json.load(f)
-        validate_dict(multi_ext_dict, pystac.STACObjectType.COLLECTION)
-        self.assertIsInstance(Collection.from_dict(multi_ext_dict), Collection)
-
-        multi_ext_col = Collection.from_file(multi_ext_uri)
-        multi_ext_col.validate()
-        ext = multi_ext_col.extent
-        extent_dict = multi_ext_dict["extent"]
-        self.assertIsInstance(ext, Extent)
-        self.assertIsInstance(ext.spatial.bboxes[0], list)
-        self.assertEqual(len(ext.spatial.bboxes), 3)
-        self.assertDictEqual(ext.to_dict(), extent_dict)
-
-        cloned_ext = ext.clone()
-        self.assertDictEqual(cloned_ext.to_dict(), multi_ext_dict["extent"])
-
-    def test_extra_fields(self) -> None:
-        catalog = TestCases.case_2()
-        collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16")
-        assert collection is not None
-
-        collection.extra_fields["test"] = "extra"
-
-        with tempfile.TemporaryDirectory() as tmp_dir:
-            p = os.path.join(tmp_dir, "collection.json")
-            collection.save_object(include_self_link=False, dest_href=p)
-            with open(p) as f:
-                col_json = json.load(f)
-            self.assertTrue("test" in col_json)
-            self.assertEqual(col_json["test"], "extra")
-
-            read_col = pystac.Collection.from_file(p)
-            self.assertTrue("test" in read_col.extra_fields)
-            self.assertEqual(read_col.extra_fields["test"], "extra")
-
-    def test_update_extents(self) -> None:
-        catalog = TestCases.case_2()
-        base_collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16")
-        assert isinstance(base_collection, Collection)
-        base_extent = base_collection.extent
-        collection = base_collection.clone()
-
-        item1 = Item(
-            id="test-item-1",
-            geometry=ARBITRARY_GEOM,
-            bbox=[-180, -90, 180, 90],
-            datetime=TEST_DATETIME,
-            properties={"key": "one"},
-            stac_extensions=["eo", "commons"],
-        )
-
-        item2 = Item(
-            id="test-item-1",
-            geometry=ARBITRARY_GEOM,
-            bbox=[-180, -90, 180, 90],
-            datetime=None,
-            properties={
-                "start_datetime": datetime_to_str(datetime(2000, 1, 1, 12, 0, 0, 0)),
-                "end_datetime": datetime_to_str(datetime(2000, 2, 1, 12, 0, 0, 0)),
-            },
-            stac_extensions=["eo", "commons"],
-        )
-
-        collection.add_item(item1)
-
-        collection.update_extent_from_items()
-        self.assertEqual([[-180, -90, 180, 90]], collection.extent.spatial.bboxes)
-        self.assertEqual(
-            len(base_extent.spatial.bboxes[0]), len(collection.extent.spatial.bboxes[0])
-        )
-
-        self.assertNotEqual(
-            base_extent.temporal.intervals, collection.extent.temporal.intervals
-        )
-        collection.remove_item("test-item-1")
-        collection.update_extent_from_items()
-        self.assertNotEqual([[-180, -90, 180, 90]], collection.extent.spatial.bboxes)
-        collection.add_item(item2)
-
-        collection.update_extent_from_items()
-
-        self.assertEqual(
-            [
-                [
-                    item2.common_metadata.start_datetime,
-                    base_extent.temporal.intervals[0][1],
-                ]
-            ],
-            collection.extent.temporal.intervals,
-        )
-
-    def test_supplying_href_in_init_does_not_fail(self) -> None:
-        test_href = "http://example.com/collection.json"
-        spatial_extent = SpatialExtent(bboxes=[ARBITRARY_BBOX])
-        temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
-
-        collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
-        collection = Collection(
-            id="test", description="test desc", extent=collection_extent, href=test_href
-        )
-
-        self.assertEqual(collection.get_self_href(), test_href)
-
-    def test_collection_with_href_caches_by_href(self) -> None:
-        collection = pystac.Collection.from_file(
-            TestCases.get_path("data-files/examples/hand-0.8.1/collection.json")
-        )
-        cache = collection._resolved_objects
-
-        # Since all of our STAC objects have HREFs, everything should be
-        # cached only by HREF
-        self.assertEqual(len(cache.id_keys_to_objects), 0)
-
-    @pytest.mark.block_network
-    def test_assets(self) -> None:
-        path = TestCases.get_path("data-files/collections/with-assets.json")
-        with open(path) as f:
-            data = json.load(f)
-        collection = pystac.Collection.from_dict(data)
-        collection.validate()
-
-    def test_get_assets(self) -> None:
-        collection = pystac.Collection.from_file(
-            TestCases.get_path("data-files/collections/with-assets.json")
-        )
-
-        media_type_filter = collection.get_assets(media_type=pystac.MediaType.PNG)
-        self.assertCountEqual(media_type_filter.keys(), ["thumbnail"])
-        role_filter = collection.get_assets(role="thumbnail")
-        self.assertCountEqual(role_filter.keys(), ["thumbnail"])
-        multi_filter = collection.get_assets(
-            media_type=pystac.MediaType.PNG, role="thumbnail"
-        )
-        self.assertCountEqual(multi_filter.keys(), ["thumbnail"])
-
-        no_filter = collection.get_assets()
-        self.assertIsNot(no_filter, collection.assets)
-        self.assertCountEqual(no_filter.keys(), ["thumbnail"])
-        no_filter["thumbnail"].description = "foo"
-        assert collection.assets["thumbnail"].description != "foo"
-
-        no_assets = collection.get_assets(media_type=pystac.MediaType.HDF)
-        self.assertEqual(no_assets, {})
-
-    def test_removing_optional_attributes(self) -> None:
-        path = TestCases.get_path("data-files/collections/with-assets.json")
-        with open(path) as file:
-            data = json.load(file)
-        data["title"] = "dummy title"
-        data["stac_extensions"] = ["dummy extension"]
-        data["keywords"] = ["key", "word"]
-        data["providers"] = [{"name": "pystac"}]
-        collection = pystac.Collection.from_dict(data)
-
-        # Assert we have everything set
-        assert collection.title
-        assert collection.stac_extensions
-        assert collection.keywords
-        assert collection.providers
-        assert collection.summaries
-        assert collection.assets
-
-        # Remove all of the optional stuff
-        collection.title = None
-        collection.stac_extensions = []
-        collection.keywords = []
-        collection.providers = []
-        collection.summaries = pystac.Summaries({})
-        collection.assets = {}
-
-        collection_as_dict = collection.to_dict()
-        for key in (
-            "title",
-            "stac_extensions",
-            "keywords",
-            "providers",
-            "summaries",
-            "assets",
-        ):
-            assert key not in collection_as_dict
-
-    def test_from_dict_preserves_dict(self) -> None:
-        path = TestCases.get_path("data-files/collections/with-assets.json")
-        with open(path) as f:
-            collection_dict = json.load(f)
-        param_dict = deepcopy(collection_dict)
-
-        # test that the parameter is preserved
-        _ = Collection.from_dict(param_dict)
-        self.assertEqual(param_dict, collection_dict)
-
-        # assert that the parameter is not preserved with
-        # non-default parameter
-        _ = Collection.from_dict(param_dict, preserve_dict=False, migrate=False)
-        self.assertNotEqual(param_dict, collection_dict)
-
-    def test_from_dict_set_root(self) -> None:
-        path = TestCases.get_path("data-files/examples/hand-0.8.1/collection.json")
-        with open(path) as f:
-            collection_dict = json.load(f)
-        catalog = pystac.Catalog(id="test", description="test desc")
-        collection = Collection.from_dict(collection_dict, root=catalog)
-        self.assertIs(collection.get_root(), catalog)
-
-    def test_schema_summary(self) -> None:
-        collection = pystac.Collection.from_file(
-            TestCases.get_path(
-                "data-files/examples/1.0.0/collection-only/collection-with-schemas.json"
-            )
-        )
-        instruments_schema = get_required(
-            collection.summaries.get_schema("instruments"),
-            collection.summaries,
-            "instruments",
-        )
-
-        self.assertIsInstance(instruments_schema, dict)
-
-    def test_from_invalid_dict_raises_exception(self) -> None:
-        stac_io = pystac.StacIO.default()
-        catalog_dict = stac_io.read_json(
-            TestCases.get_path("data-files/catalogs/test-case-1/catalog.json")
-        )
-        with self.assertRaises(pystac.STACTypeError):
-            _ = pystac.Collection.from_dict(catalog_dict)
-
-    def test_clone_preserves_assets(self) -> None:
-        path = TestCases.get_path("data-files/collections/with-assets.json")
-        original_collection = Collection.from_file(path)
-        assert len(original_collection.assets) > 0
-        assert all(
-            asset.owner is original_collection
-            for asset in original_collection.assets.values()
-        )
-
-        cloned_collection = original_collection.clone()
-
-        for key in original_collection.assets:
-            with self.subTest(f"Preserves {key} asset"):
-                self.assertIn(key, cloned_collection.assets)
-            cloned_asset = cloned_collection.assets.get(key)
-            if cloned_asset is not None:
-                with self.subTest(f"Sets owner for {key}"):
-                    self.assertIs(cloned_asset.owner, cloned_collection)
-
-    def test_to_dict_no_self_href(self) -> None:
-        temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
-        spatial_extent = SpatialExtent(bboxes=ARBITRARY_BBOX)
-        extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
-        collection = Collection(
-            id="an-id", description="A test Collection", extent=extent
+def test_provider_to_from_dict() -> None:
+    provider_dict = {
+        "name": "Remote Data, Inc",
+        "description": "Producers of awesome spatiotemporal assets",
+        "roles": ["producer", "processor"],
+        "url": "http://remotedata.io",
+        "extension:field": "some value",
+    }
+    expected_extra_fields = {"extension:field": provider_dict["extension:field"]}
+
+    provider = Provider.from_dict(provider_dict)
+
+    assert (
+        provider_dict["name"],
+        provider_dict["description"],
+        provider_dict["roles"],
+        provider_dict["url"],
+        expected_extra_fields,
+        provider_dict,
+    ) == (
+        provider.name,
+        provider.description,
+        provider.roles,
+        provider.url,
+        provider.extra_fields,
+        provider.to_dict(),
+    )
+
+
+def test_spatial_extent_from_coordinates() -> None:
+    extent = SpatialExtent.from_coordinates(ARBITRARY_GEOM["coordinates"])
+
+    assert len(extent.bboxes) == 1
+    bbox = extent.bboxes[0]
+    assert len(bbox) == 4
+    for x in bbox:
+        assert isinstance(x, float)
+
+
+def test_read_eo_items_are_heritable() -> None:
+    cat = TestCases.case_5()
+    item = next(cat.get_items(recursive=True))
+
+    assert EOExtension.has_extension(item)
+
+
+def test_save_uses_previous_catalog_type() -> None:
+    collection = TestCases.case_8()
+    assert collection.STAC_OBJECT_TYPE == pystac.STACObjectType.COLLECTION
+    assert collection.catalog_type == CatalogType.SELF_CONTAINED
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        collection.normalize_hrefs(tmp_dir)
+        href = collection.self_href
+        collection.save()
+
+        collection2 = pystac.Collection.from_file(href)
+        assert collection2.catalog_type == CatalogType.SELF_CONTAINED
+
+
+def test_clone_uses_previous_catalog_type() -> None:
+    catalog = TestCases.case_8()
+    assert catalog.catalog_type == CatalogType.SELF_CONTAINED
+    clone = catalog.clone()
+    assert clone.catalog_type == CatalogType.SELF_CONTAINED
+
+
+def test_clone_cant_mutate_original() -> None:
+    collection = TestCases.case_8()
+    assert collection.keywords == ["disaster", "open"]
+    clone = collection.clone()
+    clone.extra_fields["test"] = "extra"
+    assert "test" not in collection.extra_fields
+    assert clone.keywords is not None
+    clone.keywords.append("clone")
+    assert clone.keywords == ["disaster", "open", "clone"]
+    assert collection.keywords == ["disaster", "open"]
+    assert id(collection.summaries) != id(clone.summaries)
+
+
+def test_multiple_extents() -> None:
+    cat1 = TestCases.case_1()
+    country = cat1.get_child("country-1")
+    assert country is not None
+    col1 = country.get_child("area-1-1")
+    assert col1 is not None
+    col1.validate()
+    assert isinstance(col1, Collection)
+    validate_dict(col1.to_dict(), pystac.STACObjectType.COLLECTION)
+
+    multi_ext_uri = TestCases.get_path("data-files/collections/multi-extent.json")
+    with open(multi_ext_uri) as f:
+        multi_ext_dict = json.load(f)
+    validate_dict(multi_ext_dict, pystac.STACObjectType.COLLECTION)
+    assert isinstance(Collection.from_dict(multi_ext_dict), Collection)
+
+    multi_ext_col = Collection.from_file(multi_ext_uri)
+    multi_ext_col.validate()
+    ext = multi_ext_col.extent
+    extent_dict = multi_ext_dict["extent"]
+    assert isinstance(ext, Extent)
+    assert isinstance(ext.spatial.bboxes[0], list)
+    assert len(ext.spatial.bboxes) == 3
+    assert ext.to_dict() == extent_dict
+
+    cloned_ext = ext.clone()
+    assert cloned_ext.to_dict() == multi_ext_dict["extent"]
+
+
+def test_extra_fields() -> None:
+    catalog = TestCases.case_2()
+    collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16")
+    assert collection is not None
+
+    collection.extra_fields["test"] = "extra"
+
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        p = os.path.join(tmp_dir, "collection.json")
+        collection.save_object(include_self_link=False, dest_href=p)
+        with open(p) as f:
+            col_json = json.load(f)
+        assert "test" in col_json
+        assert col_json["test"] == "extra"
+
+        read_col = pystac.Collection.from_file(p)
+        assert "test" in read_col.extra_fields
+        assert read_col.extra_fields["test"] == "extra"
+
+
+def test_update_extents() -> None:
+    catalog = TestCases.case_2()
+    base_collection = catalog.get_child("1a8c1632-fa91-4a62-b33e-3a87c2ebdf16")
+    assert isinstance(base_collection, Collection)
+    base_extent = base_collection.extent
+    collection = base_collection.clone()
+
+    item1 = Item(
+        id="test-item-1",
+        geometry=ARBITRARY_GEOM,
+        bbox=[-180, -90, 180, 90],
+        datetime=TEST_DATETIME,
+        properties={"key": "one"},
+        stac_extensions=["eo", "commons"],
+    )
+
+    item2 = Item(
+        id="test-item-1",
+        geometry=ARBITRARY_GEOM,
+        bbox=[-180, -90, 180, 90],
+        datetime=None,
+        properties={
+            "start_datetime": datetime_to_str(datetime(2000, 1, 1, 12, 0, 0, 0)),
+            "end_datetime": datetime_to_str(datetime(2000, 2, 1, 12, 0, 0, 0)),
+        },
+        stac_extensions=["eo", "commons"],
+    )
+
+    collection.add_item(item1)
+
+    collection.update_extent_from_items()
+    assert [[-180, -90, 180, 90]] == collection.extent.spatial.bboxes
+    assert len(base_extent.spatial.bboxes[0]) == len(
+        collection.extent.spatial.bboxes[0]
+    )
+    assert base_extent.temporal.intervals != collection.extent.temporal.intervals
+
+    collection.remove_item("test-item-1")
+    collection.update_extent_from_items()
+    assert [[-180, -90, 180, 90]] != collection.extent.spatial.bboxes
+    collection.add_item(item2)
+
+    collection.update_extent_from_items()
+
+    assert [
+        [
+            item2.common_metadata.start_datetime,
+            base_extent.temporal.intervals[0][1],
+        ]
+    ] == collection.extent.temporal.intervals
+
+
+def test_supplying_href_in_init_does_not_fail() -> None:
+    test_href = "http://example.com/collection.json"
+    spatial_extent = SpatialExtent(bboxes=[ARBITRARY_BBOX])
+    temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
+
+    collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
+    collection = Collection(
+        id="test", description="test desc", extent=collection_extent, href=test_href
+    )
+
+    assert collection.get_self_href() == test_href
+
+
+def test_collection_with_href_caches_by_href() -> None:
+    collection = pystac.Collection.from_file(
+        TestCases.get_path("data-files/examples/hand-0.8.1/collection.json")
+    )
+    cache = collection._resolved_objects
+
+    # Since all of our STAC objects have HREFs, everything should be
+    # cached only by HREF
+    assert len(cache.id_keys_to_objects) == 0
+
+
+ at pytest.mark.block_network
+def test_assets() -> None:
+    path = TestCases.get_path("data-files/collections/with-assets.json")
+    with open(path) as f:
+        data = json.load(f)
+    collection = pystac.Collection.from_dict(data)
+    collection.validate()
+
+
+def test_get_assets() -> None:
+    collection = pystac.Collection.from_file(
+        TestCases.get_path("data-files/collections/with-assets.json")
+    )
+
+    media_type_filter = collection.get_assets(media_type=pystac.MediaType.PNG)
+    assert list(media_type_filter.keys()) == ["thumbnail"]
+    role_filter = collection.get_assets(role="thumbnail")
+    assert list(role_filter.keys()) == ["thumbnail"]
+    multi_filter = collection.get_assets(
+        media_type=pystac.MediaType.PNG, role="thumbnail"
+    )
+    assert list(multi_filter.keys()) == ["thumbnail"]
+
+    no_filter = collection.get_assets()
+    assert no_filter is not collection.assets
+    assert list(no_filter.keys()) == ["thumbnail"]
+    no_filter["thumbnail"].description = "foo"
+    assert collection.assets["thumbnail"].description != "foo"
+
+    no_assets = collection.get_assets(media_type=pystac.MediaType.HDF)
+    assert no_assets == {}
+
+
+def test_removing_optional_attributes() -> None:
+    path = TestCases.get_path("data-files/collections/with-assets.json")
+    with open(path) as file:
+        data = json.load(file)
+    data["title"] = "dummy title"
+    data["stac_extensions"] = ["dummy extension"]
+    data["keywords"] = ["key", "word"]
+    data["providers"] = [{"name": "pystac"}]
+    collection = pystac.Collection.from_dict(data)
+
+    # Assert we have everything set
+    assert collection.title
+    assert collection.stac_extensions
+    assert collection.keywords
+    assert collection.providers
+    assert collection.summaries
+    assert collection.assets
+
+    # Remove all of the optional stuff
+    collection.title = None
+    collection.stac_extensions = []
+    collection.keywords = []
+    collection.providers = []
+    collection.summaries = pystac.Summaries({})
+    collection.assets = {}
+
+    collection_as_dict = collection.to_dict()
+    for key in (
+        "title",
+        "stac_extensions",
+        "keywords",
+        "providers",
+        "summaries",
+        "assets",
+    ):
+        assert key not in collection_as_dict
+
+
+def test_from_dict_preserves_dict() -> None:
+    path = TestCases.get_path("data-files/collections/with-assets.json")
+    with open(path) as f:
+        collection_dict = json.load(f)
+    param_dict = deepcopy(collection_dict)
+
+    # test that the parameter is preserved
+    _ = Collection.from_dict(param_dict)
+    assert param_dict == collection_dict
+
+    # assert that the parameter is not preserved with
+    # non-default parameter
+    _ = Collection.from_dict(param_dict, preserve_dict=False, migrate=False)
+    assert param_dict != collection_dict
+
+
+def test_from_dict_set_root() -> None:
+    path = TestCases.get_path("data-files/examples/hand-0.8.1/collection.json")
+    with open(path) as f:
+        collection_dict = json.load(f)
+    catalog = pystac.Catalog(id="test", description="test desc")
+    collection = Collection.from_dict(collection_dict, root=catalog)
+    assert collection.get_root() is catalog
+
+
+def test_schema_summary() -> None:
+    collection = pystac.Collection.from_file(
+        TestCases.get_path(
+            "data-files/examples/1.0.0/collection-only/collection-with-schemas.json"
         )
-        d = collection.to_dict(include_self_link=False)
-        Collection.from_dict(d)
-
-
-class ExtentTest(unittest.TestCase):
-    def setUp(self) -> None:
-        self.maxDiff = None
-
-    def test_temporal_extent_init_typing(self) -> None:
-        # This test exists purely to test the typing of the intervals argument to
-        # TemporalExtent
-        start_datetime = str_to_datetime("2022-01-01T00:00:00Z")
-        end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
-
-        _ = TemporalExtent([[start_datetime, end_datetime]])
-
-    @pytest.mark.block_network()
-    def test_temporal_extent_allows_single_interval(self) -> None:
-        start_datetime = str_to_datetime("2022-01-01T00:00:00Z")
-        end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
-
-        interval = [start_datetime, end_datetime]
-        temporal_extent = TemporalExtent(intervals=interval)  # type: ignore
-
-        self.assertEqual(temporal_extent.intervals, [interval])
-
-    @pytest.mark.block_network()
-    def test_temporal_extent_allows_single_interval_open_start(self) -> None:
-        end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
-
-        interval = [None, end_datetime]
-        temporal_extent = TemporalExtent(intervals=interval)
-
-        self.assertEqual(temporal_extent.intervals, [interval])
-
-    @pytest.mark.block_network()
-    def test_temporal_extent_non_list_intervals_fails(self) -> None:
-        with pytest.raises(TypeError):
-            # Pass in non-list intervals
-            _ = TemporalExtent(intervals=1)  # type: ignore
-
-    @pytest.mark.block_network()
-    def test_spatial_allows_single_bbox(self) -> None:
-        temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
-
-        # Pass in a single BBOX
-        spatial_extent = SpatialExtent(bboxes=ARBITRARY_BBOX)
+    )
+    instruments_schema = get_required(
+        collection.summaries.get_schema("instruments"),
+        collection.summaries,
+        "instruments",
+    )
 
-        collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
+    assert isinstance(instruments_schema, dict)
 
-        collection = Collection(
-            id="test", description="test desc", extent=collection_extent
-        )
 
-        # HREF required by validation
-        collection.set_self_href("https://example.com/collection.json")
+def test_from_invalid_dict_raises_exception() -> None:
+    stac_io = pystac.StacIO.default()
+    catalog_dict = stac_io.read_json(
+        TestCases.get_path("data-files/catalogs/test-case-1/catalog.json")
+    )
+    with pytest.raises(pystac.STACTypeError):
+        _ = pystac.Collection.from_dict(catalog_dict)
 
-        collection.validate()
 
-    @pytest.mark.block_network()
-    def test_spatial_extent_non_list_bboxes_fails(self) -> None:
-        with pytest.raises(TypeError):
-            # Pass in non-list bboxes
-            _ = SpatialExtent(bboxes=1)  # type: ignore
+def test_clone_preserves_assets() -> None:
+    path = TestCases.get_path("data-files/collections/with-assets.json")
+    original_collection = Collection.from_file(path)
+    assert len(original_collection.assets) > 0
+    assert all(
+        asset.owner is original_collection
+        for asset in original_collection.assets.values()
+    )
 
-    def test_from_items(self) -> None:
-        item1 = Item(
-            id="test-item-1",
-            geometry=ARBITRARY_GEOM,
-            bbox=[-10, -20, 0, -10],
-            datetime=datetime(2000, 2, 1, 12, 0, 0, 0, tzinfo=tz.UTC),
-            properties={},
-        )
+    cloned_collection = original_collection.clone()
 
-        item2 = Item(
-            id="test-item-2",
-            geometry=ARBITRARY_GEOM,
-            bbox=[0, -9, 10, 1],
-            datetime=None,
-            properties={
-                "start_datetime": datetime_to_str(
-                    datetime(2000, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
-                ),
-                "end_datetime": datetime_to_str(
-                    datetime(2000, 7, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
-                ),
-            },
-        )
+    for key in original_collection.assets:
+        assert key in cloned_collection.assets, f"Failed to Preserve {key} asset"
+        cloned_asset = cloned_collection.assets.get(key)
+        if cloned_asset is not None:
+            assert (
+                cloned_asset.owner is cloned_collection
+            ), f"Failed to set owner for {key}"
 
-        item3 = Item(
-            id="test-item-2",
-            geometry=ARBITRARY_GEOM,
-            bbox=[-5, -20, 5, 0],
-            datetime=None,
-            properties={
-                "start_datetime": datetime_to_str(
-                    datetime(2000, 12, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
-                ),
-                "end_datetime": datetime_to_str(
-                    datetime(2001, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC),
-                ),
-            },
-        )
 
-        extent = Extent.from_items([item1, item2, item3])
-
-        self.assertEqual(len(extent.spatial.bboxes), 1)
-        self.assertEqual(extent.spatial.bboxes[0], [-10, -20, 10, 1])
-
-        self.assertEqual(len(extent.temporal.intervals), 1)
-        interval = extent.temporal.intervals[0]
-
-        self.assertEqual(interval[0], datetime(2000, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC))
-        self.assertEqual(interval[1], datetime(2001, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC))
-
-    def test_to_from_dict(self) -> None:
-        spatial_dict = {
-            "bbox": [
-                [
-                    172.91173669923782,
-                    1.3438851951615003,
-                    172.95469614953714,
-                    1.3690476620161975,
-                ]
-            ],
-            "extension:field": "spatial value",
-        }
-        temporal_dict = {
-            "interval": [
-                ["2020-12-11T22:38:32.125000Z", "2020-12-14T18:02:31.437000Z"]
-            ],
-            "extension:field": "temporal value",
-        }
-        extent_dict = {
-            "spatial": spatial_dict,
-            "temporal": temporal_dict,
-            "extension:field": "extent value",
-        }
-        expected_extent_extra_fields = {
-            "extension:field": extent_dict["extension:field"],
-        }
-        expected_spatial_extra_fields = {
-            "extension:field": spatial_dict["extension:field"],
-        }
-        expected_temporal_extra_fields = {
-            "extension:field": temporal_dict["extension:field"],
-        }
-
-        extent = Extent.from_dict(extent_dict)
-
-        self.assertDictEqual(expected_extent_extra_fields, extent.extra_fields)
-        self.assertDictEqual(expected_spatial_extra_fields, extent.spatial.extra_fields)
-        self.assertDictEqual(
-            expected_temporal_extra_fields, extent.temporal.extra_fields
-        )
+def test_to_dict_no_self_href() -> None:
+    temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
+    spatial_extent = SpatialExtent(bboxes=ARBITRARY_BBOX)
+    extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
+    collection = Collection(id="an-id", description="A test Collection", extent=extent)
+    d = collection.to_dict(include_self_link=False)
+    Collection.from_dict(d)
+
+
+def test_temporal_extent_init_typing() -> None:
+    # This test exists purely to test the typing of the intervals argument to
+    # TemporalExtent
+    start_datetime = str_to_datetime("2022-01-01T00:00:00Z")
+    end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
+
+    _ = TemporalExtent([[start_datetime, end_datetime]])
+
+
+ at pytest.mark.block_network()
+def test_temporal_extent_allows_single_interval() -> None:
+    start_datetime = str_to_datetime("2022-01-01T00:00:00Z")
+    end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
+
+    interval = [start_datetime, end_datetime]
+    temporal_extent = TemporalExtent(intervals=interval)  # type: ignore
+
+    assert temporal_extent.intervals == [interval]
+
+
+ at pytest.mark.block_network()
+def test_temporal_extent_allows_single_interval_open_start() -> None:
+    end_datetime = str_to_datetime("2022-01-31T23:59:59Z")
+
+    interval = [None, end_datetime]
+    temporal_extent = TemporalExtent(intervals=interval)
+
+    assert temporal_extent.intervals == [interval]
+
+
+ at pytest.mark.block_network()
+def test_temporal_extent_non_list_intervals_fails() -> None:
+    with pytest.raises(TypeError):
+        # Pass in non-list intervals
+        _ = TemporalExtent(intervals=1)  # type: ignore
+
+
+ at pytest.mark.block_network()
+def test_spatial_allows_single_bbox() -> None:
+    temporal_extent = TemporalExtent(intervals=[[TEST_DATETIME, None]])
+
+    # Pass in a single BBOX
+    spatial_extent = SpatialExtent(bboxes=ARBITRARY_BBOX)
+
+    collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)
+
+    collection = Collection(
+        id="test", description="test desc", extent=collection_extent
+    )
+
+    # HREF required by validation
+    collection.set_self_href("https://example.com/collection.json")
+
+    collection.validate()
+
+
+ at pytest.mark.block_network()
+def test_spatial_extent_non_list_bboxes_fails() -> None:
+    with pytest.raises(TypeError):
+        # Pass in non-list bboxes
+        _ = SpatialExtent(bboxes=1)  # type: ignore
+
+
+def test_extent_from_items() -> None:
+    item1 = Item(
+        id="test-item-1",
+        geometry=ARBITRARY_GEOM,
+        bbox=[-10, -20, 0, -10],
+        datetime=datetime(2000, 2, 1, 12, 0, 0, 0, tzinfo=tz.UTC),
+        properties={},
+    )
+
+    item2 = Item(
+        id="test-item-2",
+        geometry=ARBITRARY_GEOM,
+        bbox=[0, -9, 10, 1],
+        datetime=None,
+        properties={
+            "start_datetime": datetime_to_str(
+                datetime(2000, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
+            ),
+            "end_datetime": datetime_to_str(
+                datetime(2000, 7, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
+            ),
+        },
+    )
 
-        self.assertDictEqual(extent_dict, extent.to_dict())
+    item3 = Item(
+        id="test-item-2",
+        geometry=ARBITRARY_GEOM,
+        bbox=[-5, -20, 5, 0],
+        datetime=None,
+        properties={
+            "start_datetime": datetime_to_str(
+                datetime(2000, 12, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
+            ),
+            "end_datetime": datetime_to_str(
+                datetime(2001, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC),
+            ),
+        },
+    )
 
+    extent = Extent.from_items([item1, item2, item3])
+    assert len(extent.spatial.bboxes) == 1
+    assert extent.spatial.bboxes[0] == [-10, -20, 10, 1]
+    assert len(extent.temporal.intervals) == 1
 
-class CollectionSubClassTest(unittest.TestCase):
+    interval = extent.temporal.intervals[0]
+    assert interval[0] == datetime(2000, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
+    assert interval[1] == datetime(2001, 1, 1, 12, 0, 0, 0, tzinfo=tz.UTC)
+
+
+def test_extent_to_from_dict() -> None:
+    spatial_dict = {
+        "bbox": [
+            [
+                172.91173669923782,
+                1.3438851951615003,
+                172.95469614953714,
+                1.3690476620161975,
+            ]
+        ],
+        "extension:field": "spatial value",
+    }
+    temporal_dict = {
+        "interval": [["2020-12-11T22:38:32.125000Z", "2020-12-14T18:02:31.437000Z"]],
+        "extension:field": "temporal value",
+    }
+    extent_dict = {
+        "spatial": spatial_dict,
+        "temporal": temporal_dict,
+        "extension:field": "extent value",
+    }
+    expected_extent_extra_fields = {
+        "extension:field": extent_dict["extension:field"],
+    }
+    expected_spatial_extra_fields = {
+        "extension:field": spatial_dict["extension:field"],
+    }
+    expected_temporal_extra_fields = {
+        "extension:field": temporal_dict["extension:field"],
+    }
+
+    extent = Extent.from_dict(extent_dict)
+
+    assert expected_extent_extra_fields == extent.extra_fields
+    assert expected_spatial_extra_fields == extent.spatial.extra_fields
+    assert expected_temporal_extra_fields == extent.temporal.extra_fields
+
+    assert extent_dict == extent.to_dict()
+
+
+class TestCollectionSubClass:
     """This tests cases related to creating classes inheriting from pystac.Catalog to
     ensure that inheritance, class methods, etc. function as expected."""
 
@@ -537,25 +549,23 @@ class CollectionSubClassTest(unittest.TestCase):
             # backwards compatibility of inherited classes
             return super().get_items()
 
-    def setUp(self) -> None:
-        self.stac_io = pystac.StacIO.default()
-
     def test_from_dict_returns_subclass(self) -> None:
-        collection_dict = self.stac_io.read_json(self.MULTI_EXTENT)
+        stac_io = pystac.StacIO.default()
+        collection_dict = stac_io.read_json(self.MULTI_EXTENT)
         custom_collection = self.BasicCustomCollection.from_dict(collection_dict)
 
-        self.assertIsInstance(custom_collection, self.BasicCustomCollection)
+        assert isinstance(custom_collection, self.BasicCustomCollection)
 
     def test_from_file_returns_subclass(self) -> None:
         custom_collection = self.BasicCustomCollection.from_file(self.MULTI_EXTENT)
 
-        self.assertIsInstance(custom_collection, self.BasicCustomCollection)
+        assert isinstance(custom_collection, self.BasicCustomCollection)
 
     def test_clone(self) -> None:
         custom_collection = self.BasicCustomCollection.from_file(self.MULTI_EXTENT)
         cloned_collection = custom_collection.clone()
 
-        self.assertIsInstance(cloned_collection, self.BasicCustomCollection)
+        assert isinstance(cloned_collection, self.BasicCustomCollection)
 
     def test_collection_get_item_works(self) -> None:
         path = TestCases.get_path(
@@ -567,7 +577,7 @@ class CollectionSubClassTest(unittest.TestCase):
             collection.get_item("area-1-1-imagery")
 
 
-class CollectionPartialSubClassTest(unittest.TestCase):
+def test_collection_get_item_raises_type_error() -> None:
     class BasicCustomCollection(pystac.Collection):
         def get_items(  # type: ignore
             self, *, recursive: bool = False
@@ -575,14 +585,13 @@ class CollectionPartialSubClassTest(unittest.TestCase):
             # This get_items does not allow ids as args.
             return super().get_items(recursive=recursive)
 
-    def test_collection_get_item_raises_type_error(self) -> None:
-        path = TestCases.get_path(
-            "data-files/catalogs/test-case-1/country-1/area-1-1/collection.json"
-        )
-        custom_collection = self.BasicCustomCollection.from_file(path)
-        collection = custom_collection.clone()
-        with pytest.raises(TypeError, match="takes 1 positional argument"):
-            collection.get_item("area-1-1-imagery")
+    path = TestCases.get_path(
+        "data-files/catalogs/test-case-1/country-1/area-1-1/collection.json"
+    )
+    custom_collection = BasicCustomCollection.from_file(path)
+    collection = custom_collection.clone()
+    with pytest.raises(TypeError, match="takes 1 positional argument"):
+        collection.get_item("area-1-1-imagery")
 
 
 def test_custom_collection_from_dict(collection: Collection) -> None:


=====================================
tests/test_item.py
=====================================
@@ -5,10 +5,9 @@ import json
 import os
 import pickle
 import tempfile
-import unittest
 from copy import deepcopy
 from pathlib import Path
-from typing import Any
+from typing import Any, cast
 
 import dateutil.relativedelta
 import pytest
@@ -27,343 +26,321 @@ from pystac.validation import validate_dict
 from tests.utils import TestCases, assert_to_from_dict
 
 
-class ItemTest(unittest.TestCase):
-    def get_example_item_dict(self) -> dict[str, Any]:
-        m = TestCases.get_path("data-files/item/sample-item.json")
-        with open(m) as f:
-            item_dict: dict[str, Any] = json.load(f)
-        return item_dict
+def test_to_from_dict(sample_item_dict: dict[str, Any]) -> None:
+    param_dict = deepcopy(sample_item_dict)
 
-    def test_to_from_dict(self) -> None:
-        self.maxDiff = None
+    assert_to_from_dict(Item, param_dict)
+    item = Item.from_dict(param_dict)
+    assert item.id == "CS3-20160503_132131_05"
 
-        item_dict = self.get_example_item_dict()
-        param_dict = deepcopy(item_dict)
+    # test asset creation additional field(s)
+    assert (
+        item.assets["analytic"].extra_fields["product"]
+        == "http://cool-sat.com/catalog/products/analytic.json"
+    )
+    assert len(item.assets["thumbnail"].extra_fields) == 0
+
+    # test that the parameter is preserved
+    assert param_dict == sample_item_dict
+
+    # assert that the parameter is preserved regardless of preserve_dict
+    Item.from_dict(param_dict, preserve_dict=False)
+    assert param_dict == sample_item_dict
+
+
+def test_from_dict_set_root(sample_item_dict: dict[str, Any]) -> None:
+    catalog = pystac.Catalog(id="test", description="test desc")
+    item = Item.from_dict(sample_item_dict, root=catalog)
+    assert item.get_root() is catalog
+
+
+def test_set_self_href_does_not_break_asset_hrefs() -> None:
+    cat = TestCases.case_2()
+    for item in cat.get_items(recursive=True):
+        for asset in item.assets.values():
+            if is_absolute_href(asset.href):
+                asset.href = f"./{os.path.basename(asset.href)}"
+        item.set_self_href("http://example.com/item.json")
+        for asset in item.assets.values():
+            assert is_absolute_href(asset.href)
+
+
+def test_set_self_href_none_ignores_relative_asset_hrefs() -> None:
+    cat = TestCases.case_2()
+    for item in cat.get_items(recursive=True):
+        for asset in item.assets.values():
+            if is_absolute_href(asset.href):
+                asset.href = f"./{os.path.basename(asset.href)}"
+        item.set_self_href(None)
+        for asset in item.assets.values():
+            assert not is_absolute_href(asset.href)
+
+
+def test_asset_absolute_href(sample_item: Item) -> None:
+    item_path = TestCases.get_path("data-files/item/sample-item.json")
+    sample_item.set_self_href(item_path)
+    rel_asset = Asset("./data.geojson")
+    rel_asset.set_owner(sample_item)
+    expected_href = make_posix_style(
+        os.path.abspath(os.path.join(os.path.dirname(item_path), "./data.geojson"))
+    )
+    actual_href = rel_asset.get_absolute_href()
+    assert expected_href == actual_href
 
-        assert_to_from_dict(Item, param_dict)
-        item = Item.from_dict(param_dict)
-        self.assertEqual(item.id, "CS3-20160503_132131_05")
 
-        # test asset creation additional field(s)
-        self.assertEqual(
-            item.assets["analytic"].extra_fields["product"],
-            "http://cool-sat.com/catalog/products/analytic.json",
-        )
-        self.assertEqual(len(item.assets["thumbnail"].extra_fields), 0)
-
-        # test that the parameter is preserved
-        self.assertEqual(param_dict, item_dict)
-
-        # assert that the parameter is preserved regardless of
-        # preserve_dict
-        _ = Item.from_dict(param_dict, preserve_dict=False)
-        self.assertEqual(param_dict, item_dict)
-
-    def test_from_dict_set_root(self) -> None:
-        item_dict = self.get_example_item_dict()
-        catalog = pystac.Catalog(id="test", description="test desc")
-        item = Item.from_dict(item_dict, root=catalog)
-        self.assertIs(item.get_root(), catalog)
-
-    def test_set_self_href_does_not_break_asset_hrefs(self) -> None:
-        cat = TestCases.case_2()
-        for item in cat.get_items(recursive=True):
-            for asset in item.assets.values():
-                if is_absolute_href(asset.href):
-                    asset.href = f"./{os.path.basename(asset.href)}"
-            item.set_self_href("http://example.com/item.json")
-            for asset in item.assets.values():
-                self.assertTrue(is_absolute_href(asset.href))
-
-    def test_set_self_href_none_ignores_relative_asset_hrefs(self) -> None:
-        cat = TestCases.case_2()
-        for item in cat.get_items(recursive=True):
-            for asset in item.assets.values():
-                if is_absolute_href(asset.href):
-                    asset.href = f"./{os.path.basename(asset.href)}"
-            item.set_self_href(None)
-            for asset in item.assets.values():
-                self.assertFalse(is_absolute_href(asset.href))
-
-    def test_asset_absolute_href(self) -> None:
-        item_path = TestCases.get_path("data-files/item/sample-item.json")
-        item_dict = self.get_example_item_dict()
-        item = Item.from_dict(item_dict)
-        item.set_self_href(item_path)
-        rel_asset = Asset("./data.geojson")
-        rel_asset.set_owner(item)
-        expected_href = make_posix_style(
-            os.path.abspath(os.path.join(os.path.dirname(item_path), "./data.geojson"))
-        )
-        actual_href = rel_asset.get_absolute_href()
-        self.assertEqual(expected_href, actual_href)
-
-    def test_asset_absolute_href_no_item_self(self) -> None:
-        item_dict = self.get_example_item_dict()
-        item = Item.from_dict(item_dict)
-        assert item.get_self_href() is None
-
-        rel_asset = Asset("./data.geojson")
-        rel_asset.set_owner(item)
-        actual_href = rel_asset.get_absolute_href()
-        self.assertEqual(None, actual_href)
-
-    def test_item_field_order(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item.json")
-        )
-        item_dict = item.to_dict(include_self_link=False)
-        expected_order = [
-            "type",
-            "stac_version",
-            "stac_extensions",
-            "id",
-            "geometry",
-            "bbox",
-            "properties",
-            "links",
-            "assets",
-            "collection",
-        ]
-        actual_order = list(item_dict.keys())
-        self.assertEqual(
-            actual_order,
-            expected_order,
-            f"Order was {actual_order}, expected {expected_order}",
-        )
+def test_asset_absolute_href_no_item_self(sample_item_dict: dict[str, Any]) -> None:
+    item = Item.from_dict(sample_item_dict)
+    assert item.get_self_href() is None
 
-    def test_extra_fields(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item.json")
-        )
+    rel_asset = Asset("./data.geojson")
+    rel_asset.set_owner(item)
+    actual_href = rel_asset.get_absolute_href()
+    assert actual_href is None
 
-        item.extra_fields["test"] = "extra"
-
-        with tempfile.TemporaryDirectory() as tmp_dir:
-            p = os.path.join(tmp_dir, "item.json")
-            item.save_object(include_self_link=False, dest_href=p)
-            with open(p) as f:
-                item_json = json.load(f)
-            self.assertTrue("test" in item_json)
-            self.assertEqual(item_json["test"], "extra")
-
-            read_item = pystac.Item.from_file(p)
-            self.assertTrue("test" in read_item.extra_fields)
-            self.assertEqual(read_item.extra_fields["test"], "extra")
-
-    def test_clearing_collection(self) -> None:
-        collection = TestCases.case_4().get_child("acc")
-        assert isinstance(collection, pystac.Collection)
-        item = next(collection.get_items(recursive=True))
-        self.assertEqual(item.collection_id, collection.id)
-        item.set_collection(None)
-        self.assertIsNone(item.collection_id)
-        self.assertIsNone(item.get_collection())
-        item.set_collection(collection)
-        self.assertEqual(item.collection_id, collection.id)
-        self.assertIs(item.get_collection(), collection)
-
-    def test_datetime_ISO8601_format(self) -> None:
-        item_dict = self.get_example_item_dict()
-
-        item = Item.from_dict(item_dict)
-
-        formatted_time = item.to_dict()["properties"]["datetime"]
-
-        self.assertEqual("2016-05-03T13:22:30.040000Z", formatted_time)
-
-    @pytest.mark.vcr()
-    def test_null_datetime(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item.json")
-        )
 
-        with self.assertRaises(pystac.STACError):
-            Item(
-                "test",
-                geometry=item.geometry,
-                bbox=item.bbox,
-                datetime=None,
-                properties={},
-            )
+def test_item_field_order() -> None:
+    item = pystac.Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))
+    item_dict = item.to_dict(include_self_link=False)
+    expected_order = [
+        "type",
+        "stac_version",
+        "stac_extensions",
+        "id",
+        "geometry",
+        "bbox",
+        "properties",
+        "links",
+        "assets",
+        "collection",
+    ]
+    actual_order = list(item_dict.keys())
+    assert actual_order == expected_order
+
 
-        null_dt_item = Item(
+def test_extra_fields() -> None:
+    item = pystac.Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))
+
+    item.extra_fields["test"] = "extra"
+
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        p = os.path.join(tmp_dir, "item.json")
+        item.save_object(include_self_link=False, dest_href=p)
+        with open(p) as f:
+            item_json = json.load(f)
+        assert "test" in item_json
+        assert item_json["test"] == "extra"
+
+        read_item = pystac.Item.from_file(p)
+        assert "test" in read_item.extra_fields
+        assert read_item.extra_fields["test"] == "extra"
+
+
+def test_clearing_collection() -> None:
+    collection = TestCases.case_4().get_child("acc")
+    assert isinstance(collection, pystac.Collection)
+    item = next(collection.get_items(recursive=True))
+    assert item.collection_id == collection.id
+    item.set_collection(None)
+    assert item.collection_id is None
+    assert item.get_collection() is None
+    item.set_collection(collection)
+    assert item.collection_id == collection.id
+    assert item.get_collection() is collection
+
+
+def test_datetime_ISO8601_format(sample_item: Item) -> None:
+    formatted_time = sample_item.to_dict()["properties"]["datetime"]
+    assert "2016-05-03T13:22:30.040000Z" == formatted_time
+
+
+ at pytest.mark.vcr()
+def test_null_datetime() -> None:
+    item = pystac.Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))
+
+    with pytest.raises(pystac.STACError):
+        Item(
             "test",
             geometry=item.geometry,
             bbox=item.bbox,
             datetime=None,
-            properties={
-                "start_datetime": datetime_to_str(get_opt(item.datetime)),
-                "end_datetime": datetime_to_str(get_opt(item.datetime)),
-            },
+            properties={},
         )
 
-        null_dt_item.validate()
+    null_dt_item = Item(
+        "test",
+        geometry=item.geometry,
+        bbox=item.bbox,
+        datetime=None,
+        properties={
+            "start_datetime": datetime_to_str(get_opt(item.datetime)),
+            "end_datetime": datetime_to_str(get_opt(item.datetime)),
+        },
+    )
+
+    null_dt_item.validate()
 
-    def test_get_assets(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item.json")
-        )
 
-        media_type_filter = item.get_assets(media_type=pystac.MediaType.COG)
-        self.assertCountEqual(media_type_filter.keys(), ["analytic"])
-        role_filter = item.get_assets(role="data")
-        self.assertCountEqual(role_filter.keys(), ["analytic"])
-        multi_filter = item.get_assets(
-            media_type=pystac.MediaType.PNG, role="thumbnail"
-        )
-        self.assertCountEqual(multi_filter.keys(), ["thumbnail"])
-        multi_filter["thumbnail"].description = "foo"
-        assert item.assets["thumbnail"].description != "foo"
-
-        no_filter = item.get_assets()
-        self.assertCountEqual(no_filter.keys(), ["analytic", "thumbnail"])
-        no_assets = item.get_assets(media_type=pystac.MediaType.HDF)
-        self.assertEqual(no_assets, {})
-
-    @pytest.mark.vcr()
-    def test_null_datetime_constructor(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item.json")
+def test_get_assets() -> None:
+    item = pystac.Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))
+
+    media_type_filter = item.get_assets(media_type=pystac.MediaType.COG)
+    assert set(media_type_filter.keys()) == {"analytic"}
+    role_filter = item.get_assets(role="data")
+    assert set(role_filter.keys()) == {"analytic"}
+    multi_filter = item.get_assets(media_type=pystac.MediaType.PNG, role="thumbnail")
+    assert set(multi_filter.keys()) == {"thumbnail"}
+    multi_filter["thumbnail"].description = "foo"
+    assert item.assets["thumbnail"].description != "foo"
+
+    no_filter = item.get_assets()
+    assert set(no_filter.keys()) == {"analytic", "thumbnail"}
+    no_assets = item.get_assets(media_type=pystac.MediaType.HDF)
+    assert no_assets == {}
+
+
+ at pytest.mark.vcr()
+def test_null_datetime_constructor() -> None:
+    item = pystac.Item.from_file(TestCases.get_path("data-files/item/sample-item.json"))
+    with pytest.raises(pystac.STACError):
+        Item(
+            "test",
+            geometry=item.geometry,
+            bbox=item.bbox,
+            datetime=None,
+            end_datetime=item.datetime,
+            properties={},
         )
-        with self.assertRaises(pystac.STACError):
-            Item(
-                "test",
-                geometry=item.geometry,
-                bbox=item.bbox,
-                datetime=None,
-                end_datetime=item.datetime,
-                properties={},
-            )
-        with self.assertRaises(pystac.STACError):
-            Item(
-                "test",
-                geometry=item.geometry,
-                bbox=item.bbox,
-                datetime=None,
-                start_datetime=item.datetime,
-                properties={},
-            )
-        assert item.datetime
-        null_dt_item = Item(
+    with pytest.raises(pystac.STACError):
+        Item(
             "test",
             geometry=item.geometry,
             bbox=item.bbox,
             datetime=None,
             start_datetime=item.datetime,
-            end_datetime=item.datetime + dateutil.relativedelta.relativedelta(days=1),
             properties={},
         )
-        null_dt_item.validate()
+    assert item.datetime
+    null_dt_item = Item(
+        "test",
+        geometry=item.geometry,
+        bbox=item.bbox,
+        datetime=None,
+        start_datetime=item.datetime,
+        end_datetime=item.datetime + dateutil.relativedelta.relativedelta(days=1),
+        properties={},
+    )
+    null_dt_item.validate()
 
-    def test_get_set_asset_datetime(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path("data-files/item/sample-item-asset-properties.json")
-        )
-        item_datetime = item.datetime
 
-        # No property on asset
-        self.assertEqual(item.get_datetime(item.assets["thumbnail"]), item.datetime)
+def test_get_set_asset_datetime() -> None:
+    item = pystac.Item.from_file(
+        TestCases.get_path("data-files/item/sample-item-asset-properties.json")
+    )
+    item_datetime = item.datetime
 
-        # Property on asset
-        self.assertNotEqual(item.get_datetime(item.assets["analytic"]), item.datetime)
-        self.assertEqual(
-            item.get_datetime(item.assets["analytic"]),
-            str_to_datetime("2017-05-03T13:22:30.040Z"),
-        )
+    # No property on asset
+    assert item.get_datetime(item.assets["thumbnail"]) == item.datetime
 
-        item.set_datetime(
-            str_to_datetime("2018-05-03T13:22:30.040Z"), item.assets["thumbnail"]
-        )
-        self.assertEqual(item.get_datetime(), item_datetime)
-        self.assertEqual(
-            item.get_datetime(item.assets["thumbnail"]),
-            str_to_datetime("2018-05-03T13:22:30.040Z"),
-        )
+    # Property on asset
+    assert item.get_datetime(item.assets["analytic"]) != item.datetime
+    assert item.get_datetime(item.assets["analytic"]) == str_to_datetime(
+        "2017-05-03T13:22:30.040Z"
+    )
 
-    def test_read_eo_item_owns_asset(self) -> None:
-        item = next(TestCases.case_1().get_items(recursive=True))
-        assert len(item.assets) > 0
-        for asset_key in item.assets:
-            self.assertEqual(item.assets[asset_key].owner, item)
+    item.set_datetime(
+        str_to_datetime("2018-05-03T13:22:30.040Z"), item.assets["thumbnail"]
+    )
+    assert item.get_datetime() == item_datetime
+    assert item.get_datetime(item.assets["thumbnail"]) == str_to_datetime(
+        "2018-05-03T13:22:30.040Z"
+    )
 
-    @pytest.mark.vcr()
-    def test_null_geometry(self) -> None:
-        m = TestCases.get_path(
-            "data-files/examples/1.0.0-beta.2/item-spec/examples/null-geom-item.json"
-        )
-        with open(m) as f:
-            item_dict = json.load(f)
 
-        validate_dict(item_dict, pystac.STACObjectType.ITEM)
+def test_read_eo_item_owns_asset() -> None:
+    item = next(TestCases.case_1().get_items(recursive=True))
+    assert len(item.assets) > 0
+    for asset_key in item.assets:
+        assert item.assets[asset_key].owner == item
 
-        item = Item.from_dict(item_dict)
-        self.assertIsInstance(item, Item)
-        item.validate()
 
-        item_dict = item.to_dict()
-        self.assertIsNone(item_dict["geometry"])
-        self.assertNotIn("bbox", item_dict)
+ at pytest.mark.vcr()
+def test_null_geometry() -> None:
+    m = TestCases.get_path(
+        "data-files/examples/1.0.0-beta.2/item-spec/examples/null-geom-item.json"
+    )
+    with open(m) as f:
+        item_dict = json.load(f)
 
-    def test_0_9_item_with_no_extensions_does_not_read_collection_data(self) -> None:
-        item_json = pystac.StacIO.default().read_json(
-            TestCases.get_path("data-files/examples/hand-0.9.0/010100/010100.json")
-        )
-        assert item_json.get("stac_extensions") is None
-        assert item_json.get("stac_version") == "0.9.0"
+    validate_dict(item_dict, pystac.STACObjectType.ITEM)
 
-        did_merge = pystac.serialization.common_properties.merge_common_properties(
-            item_json
-        )
-        self.assertFalse(did_merge)
-
-    def test_clone_preserves_assets(self) -> None:
-        cat = TestCases.case_2()
-        original_item = next(cat.get_items(recursive=True))
-        assert len(original_item.assets) > 0
-        assert all(
-            asset.owner is original_item for asset in original_item.assets.values()
-        )
+    item = Item.from_dict(item_dict)
+    assert isinstance(item, Item)
+    item.validate()
 
-        cloned_item = original_item.clone()
+    item_dict = item.to_dict()
+    assert item_dict["geometry"] is None
+    assert "bbox" not in item_dict
 
-        for key in original_item.assets:
-            with self.subTest(f"Preserves {key} asset"):
-                self.assertIn(key, cloned_item.assets)
-            cloned_asset = cloned_item.assets.get(key)
-            if cloned_asset is not None:
-                with self.subTest(f"Sets owner for {key}"):
-                    self.assertIs(cloned_asset.owner, cloned_item)
 
-    def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None:
-        cat = TestCases.case_2()
-        item = next(cat.get_items(recursive=True))
-        asset = list(item.assets.values())[0]
-        assert not is_absolute_href(asset.href)
-        original_href = asset.get_absolute_href()
+def test_0_9_item_with_no_extensions_does_not_read_collection_data() -> None:
+    item_json = pystac.StacIO.default().read_json(
+        TestCases.get_path("data-files/examples/hand-0.9.0/010100/010100.json")
+    )
+    assert item_json.get("stac_extensions") is None
+    assert item_json.get("stac_version") == "0.9.0"
 
-        item.make_asset_hrefs_relative()
-        self.assertEqual(asset.get_absolute_href(), original_href)
+    did_merge = pystac.serialization.common_properties.merge_common_properties(
+        item_json
+    )
+    assert not did_merge
 
-    def test_from_invalid_dict_raises_exception(self) -> None:
-        stac_io = pystac.StacIO.default()
-        catalog_dict = stac_io.read_json(
-            TestCases.get_path("data-files/catalogs/test-case-1/catalog.json")
-        )
-        with self.assertRaises(pystac.STACTypeError):
-            _ = pystac.Item.from_dict(catalog_dict)
-
-    @pytest.mark.vcr()
-    def test_relative_extension_path(self) -> None:
-        item = pystac.Item.from_file(
-            TestCases.get_path(
-                "data-files/item/sample-item-with-relative-extension-path.json"
-            )
+
+def test_clone_preserves_assets() -> None:
+    cat = TestCases.case_2()
+    original_item = next(cat.get_items(recursive=True))
+    assert len(original_item.assets) > 0
+    assert all(asset.owner is original_item for asset in original_item.assets.values())
+
+    cloned_item = original_item.clone()
+
+    for key in original_item.assets:
+        assert key in cloned_item.assets, f"Failed to preserve asset {key}"
+        cloned_asset = cloned_item.assets.get(key)
+        if cloned_asset is not None:
+            assert cloned_asset.owner is cloned_item, f"Failed set owner for {key}"
+
+
+def test_make_asset_href_relative_is_noop_on_relative_hrefs() -> None:
+    cat = TestCases.case_2()
+    item = next(cat.get_items(recursive=True))
+    asset = list(item.assets.values())[0]
+    assert not is_absolute_href(asset.href)
+    original_href = asset.get_absolute_href()
+
+    item.make_asset_hrefs_relative()
+    assert asset.get_absolute_href() == original_href
+
+
+def test_from_invalid_dict_raises_exception() -> None:
+    stac_io = pystac.StacIO.default()
+    catalog_dict = stac_io.read_json(
+        TestCases.get_path("data-files/catalogs/test-case-1/catalog.json")
+    )
+    with pytest.raises(pystac.STACTypeError):
+        _ = pystac.Item.from_dict(catalog_dict)
+
+
+ at pytest.mark.vcr()
+def test_relative_extension_path() -> None:
+    item = pystac.Item.from_file(
+        TestCases.get_path(
+            "data-files/item/sample-item-with-relative-extension-path.json"
         )
-        item.validate()
+    )
+    item.validate()
 
 
-class ItemSubClassTest(unittest.TestCase):
+class TestItemSubClass:
     """This tests cases related to creating classes inheriting from pystac.Catalog to
     ensure that inheritance, class methods, etc. function as expected."""
 
@@ -372,87 +349,66 @@ class ItemSubClassTest(unittest.TestCase):
     class BasicCustomItem(pystac.Item):
         pass
 
-    def setUp(self) -> None:
-        self.stac_io = pystac.StacIO.default()
-
     def test_from_dict_returns_subclass(self) -> None:
-        item_dict = self.stac_io.read_json(self.SAMPLE_ITEM)
+        stac_io = pystac.StacIO.default()
+        item_dict = stac_io.read_json(self.SAMPLE_ITEM)
         custom_item = self.BasicCustomItem.from_dict(item_dict)
 
-        self.assertIsInstance(custom_item, self.BasicCustomItem)
+        assert isinstance(custom_item, self.BasicCustomItem)
 
     def test_from_file_returns_subclass(self) -> None:
         custom_item = self.BasicCustomItem.from_file(self.SAMPLE_ITEM)
 
-        self.assertIsInstance(custom_item, self.BasicCustomItem)
+        assert isinstance(custom_item, self.BasicCustomItem)
 
     def test_clone(self) -> None:
         custom_item = self.BasicCustomItem.from_file(self.SAMPLE_ITEM)
         cloned_item = custom_item.clone()
 
-        self.assertIsInstance(cloned_item, self.BasicCustomItem)
-
+        assert isinstance(cloned_item, self.BasicCustomItem)
 
-class AssetTest(unittest.TestCase):
-    def setUp(self) -> None:
-        self.maxDiff = None
-        with open(TestCases.get_path("data-files/item/sample-item.json")) as src:
-            item_dict = json.load(src)
 
-        self.asset_dict = item_dict["assets"]["analytic"]
+def test_asset_clone() -> None:
+    with open(TestCases.get_path("data-files/item/sample-item.json")) as src:
+        item_dict = json.load(src)
+    asset_dict = item_dict["assets"]["analytic"]
+    original_asset = Asset.from_dict(asset_dict)
 
-    def example_asset(self) -> Asset:
-        return Asset.from_dict(self.asset_dict)
+    cloned_asset = original_asset.clone()
 
-    def test_clone(self) -> None:
-        original_asset = self.example_asset()
-        cloned_asset = original_asset.clone()
+    assert original_asset.to_dict() == asset_dict
+    assert cloned_asset.to_dict() == asset_dict
 
-        self.assertDictEqual(original_asset.to_dict(), self.asset_dict)
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
+    # Changes to original asset should not affect cloned Asset
+    original_asset.description = "Some new description"
+    original_asset.href = "/path/to/new/href"
+    original_asset.title = "New Title"
+    original_asset.roles = ["new role"]
+    original_asset.roles.append("new role")
+    original_asset.extra_fields["new_field"] = "new_value"
+    assert cloned_asset.to_dict() == asset_dict
 
-        # Changes to original asset should not affect cloned Asset
-        original_asset.description = "Some new description"
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
 
-        original_asset.href = "/path/to/new/href"
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
-
-        original_asset.title = "New Title"
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
-
-        original_asset.roles = ["new role"]
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
-
-        original_asset.roles.append("new role")
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
-
-        original_asset.extra_fields["new_field"] = "new_value"
-        self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict)
-
-
-class AssetSubClassTest(unittest.TestCase):
+class TestAssetSubClass:
     class CustomAsset(Asset):
         pass
 
-    def setUp(self) -> None:
-        self.maxDiff = None
+    AssetDict = dict[str, str | list[str]]
+
+    @pytest.fixture
+    def asset_dict(self) -> AssetDict:
         with open(TestCases.get_path("data-files/item/sample-item.json")) as src:
             item_dict = json.load(src)
+        return cast(TestAssetSubClass.AssetDict, item_dict["assets"]["analytic"])
 
-        self.asset_dict = item_dict["assets"]["analytic"]
-
-    def test_from_dict(self) -> None:
-        asset = self.CustomAsset.from_dict(self.asset_dict)
-
-        self.assertIsInstance(asset, self.CustomAsset)
+    def test_from_dict(self, asset_dict: AssetDict) -> None:
+        asset = self.CustomAsset.from_dict(asset_dict)
+        assert isinstance(asset, self.CustomAsset)
 
-    def test_clone(self) -> None:
-        asset = self.CustomAsset.from_dict(self.asset_dict)
+    def test_clone(self, asset_dict: AssetDict) -> None:
+        asset = self.CustomAsset.from_dict(asset_dict)
         cloned_asset = asset.clone()
-
-        self.assertIsInstance(cloned_asset, self.CustomAsset)
-        self.assertIsInstance(cloned_asset, self.CustomAsset)
+        assert isinstance(cloned_asset, self.CustomAsset)
 
 
 def test_custom_item_from_dict(item: Item) -> None:


=====================================
tests/test_item_assets.py
=====================================
@@ -1,5 +1,3 @@
-import unittest
-
 import pytest
 
 from pystac import Collection
@@ -13,43 +11,18 @@ CLASSIFICATION_COLLECTION_RASTER_URI = TestCases.get_path(
 )
 
 
-class TestItemAssets(unittest.TestCase):
-    def setUp(self) -> None:
-        self.maxDiff = None
-        self.collection = Collection.from_file(
-            TestCases.get_path("data-files/item-assets/example-landsat8.json")
-        )
-
-    def test_example(self) -> None:
-        collection = self.collection.clone()
-
-        self.assertEqual(len(collection.item_assets), 13)
-
-        self.assertEqual(
-            collection.item_assets["B1"],
-            ItemAssetDefinition(
-                {
-                    "type": "image/tiff; application=geotiff",
-                    "eo:bands": [
-                        {
-                            "name": "B1",
-                            "common_name": "coastal",
-                            "center_wavelength": 0.44,
-                            "full_width_half_max": 0.02,
-                        }
-                    ],
-                    "title": "Coastal Band (B1)",
-                    "description": "Coastal Band Top Of the Atmosphere",
-                }
-            ),
-        )
+ at pytest.fixture
+def landsat8_collection() -> Collection:
+    return Collection.from_file(
+        TestCases.get_path("data-files/item-assets/example-landsat8.json")
+    )
 
-    def test_set_using_dict(self) -> None:
-        collection = self.collection.clone()
 
-        self.assertEqual(len(collection.item_assets), 13)
+def test_example(landsat8_collection: Collection) -> None:
+    assert len(landsat8_collection.item_assets) == 13
 
-        collection.item_assets["Bx"] = {
+    assert landsat8_collection.item_assets["B1"] == ItemAssetDefinition(
+        {
             "type": "image/tiff; application=geotiff",
             "eo:bands": [
                 {
@@ -61,20 +34,35 @@ class TestItemAssets(unittest.TestCase):
             ],
             "title": "Coastal Band (B1)",
             "description": "Coastal Band Top Of the Atmosphere",
-        }  # type:ignore
+        }
+    )
 
-        self.assertEqual(collection.item_assets["B1"], collection.item_assets["Bx"])
 
+def test_set_using_dict(landsat8_collection: Collection) -> None:
+    assert len(landsat8_collection.item_assets) == 13
 
-class TestAssetDefinition(unittest.TestCase):
-    def setUp(self) -> None:
-        self.maxDiff = None
-        self.collection = Collection.from_file(
-            TestCases.get_path("data-files/item-assets/example-landsat8.json")
-        )
+    landsat8_collection.item_assets["Bx"] = {
+        "type": "image/tiff; application=geotiff",
+        "eo:bands": [
+            {
+                "name": "B1",
+                "common_name": "coastal",
+                "center_wavelength": 0.44,
+                "full_width_half_max": 0.02,
+            }
+        ],
+        "title": "Coastal Band (B1)",
+        "description": "Coastal Band Top Of the Atmosphere",
+    }  # type:ignore
+
+    assert (
+        landsat8_collection.item_assets["B1"] == landsat8_collection.item_assets["Bx"]
+    )
 
-    def test_eq(self) -> None:
-        assert self.collection.item_assets["B1"] != {"title": "Coastal Band (B1)"}
+
+class TestAssetDefinition:
+    def test_eq(self, landsat8_collection: Collection) -> None:
+        assert landsat8_collection.item_assets["B1"] != {"title": "Coastal Band (B1)"}
 
     def test_create(self) -> None:
         title = "Coastal Band (B1)"
@@ -84,10 +72,12 @@ class TestAssetDefinition(unittest.TestCase):
         asset_defn = ItemAssetDefinition.create(
             title=title, description=description, media_type=media_type, roles=roles
         )
-        self.assertEqual(asset_defn.title, title)
-        self.assertEqual(asset_defn.description, description)
-        self.assertEqual(asset_defn.media_type, media_type)
-        self.assertEqual(asset_defn.roles, roles)
+        assert (
+            asset_defn.title,
+            asset_defn.description,
+            asset_defn.media_type,
+            asset_defn.roles,
+        ) == (title, description, media_type, roles)
 
     def test_title(self) -> None:
         asset_defn = ItemAssetDefinition({})
@@ -95,8 +85,7 @@ class TestAssetDefinition(unittest.TestCase):
 
         asset_defn.title = title
 
-        self.assertEqual(asset_defn.title, title)
-        self.assertEqual(asset_defn.to_dict()["title"], title)
+        assert asset_defn.title == asset_defn.to_dict()["title"] == title
 
     def test_description(self) -> None:
         asset_defn = ItemAssetDefinition({})
@@ -104,8 +93,9 @@ class TestAssetDefinition(unittest.TestCase):
 
         asset_defn.description = description
 
-        self.assertEqual(asset_defn.description, description)
-        self.assertEqual(asset_defn.to_dict()["description"], description)
+        assert (
+            asset_defn.description == asset_defn.to_dict()["description"] == description
+        )
 
     def test_media_type(self) -> None:
         asset_defn = ItemAssetDefinition({})
@@ -113,8 +103,7 @@ class TestAssetDefinition(unittest.TestCase):
 
         asset_defn.media_type = media_type
 
-        self.assertEqual(asset_defn.media_type, media_type)
-        self.assertEqual(asset_defn.to_dict()["type"], media_type)
+        assert asset_defn.media_type == asset_defn.to_dict()["type"] == media_type
 
     def test_roles(self) -> None:
         asset_defn = ItemAssetDefinition({})
@@ -122,10 +111,9 @@ class TestAssetDefinition(unittest.TestCase):
 
         asset_defn.roles = roles
 
-        self.assertEqual(asset_defn.roles, roles)
-        self.assertEqual(asset_defn.to_dict()["roles"], roles)
+        assert asset_defn.roles == asset_defn.to_dict()["roles"] == roles
 
-    def test_set_owner(self) -> None:
+    def test_set_owner(self, landsat8_collection: Collection) -> None:
         asset_definition = ItemAssetDefinition(
             {
                 "type": "image/tiff; application=geotiff",
@@ -141,8 +129,8 @@ class TestAssetDefinition(unittest.TestCase):
                 "description": "Coastal Band Top Of the Atmosphere",
             }
         )
-        asset_definition.set_owner(self.collection)
-        assert asset_definition.owner == self.collection
+        asset_definition.set_owner(landsat8_collection)
+        assert asset_definition.owner == landsat8_collection
 
 
 def test_extra_fields(collection: Collection) -> None:


=====================================
tests/test_item_collection.py
=====================================
@@ -1,186 +1,208 @@
 import json
-import unittest
 from copy import deepcopy
 from os.path import relpath
+from typing import Any, cast
+
+import pytest
 
 import pystac
+from pystac import Item, StacIO
 from pystac.item_collection import ItemCollection
 from tests.utils import TestCases
 from tests.utils.stac_io_mock import MockDefaultStacIO
 
+SIMPLE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/simple-item.json")
+CORE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/core-item.json")
+EXTENDED_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/extended-item.json")
+
+ITEM_COLLECTION = TestCases.get_path(
+    "data-files/item-collection/sample-item-collection.json"
+)
+
+
+ at pytest.fixture
+def item_collection_dict() -> dict[str, Any]:
+    with open(ITEM_COLLECTION) as src:
+        return cast(dict[str, Any], json.load(src))
+
+
+ at pytest.fixture
+def items(item_collection_dict: dict[str, Any]) -> list[Item]:
+    return [Item.from_dict(f) for f in item_collection_dict["features"]]
+
+
+ at pytest.fixture
+def stac_io() -> StacIO:
+    return StacIO.default()
+
+
+def test_item_collection_length(
+    item_collection_dict: dict[str, Any], items: list[Item]
+) -> None:
+    item_collection = pystac.ItemCollection(items=items)
+
+    assert len(item_collection) == len(items)
+
+
+def test_item_collection_iter(items: list[Item]) -> None:
+    expected_ids = [item.id for item in items]
+    item_collection = pystac.ItemCollection(items=items)
+
+    actual_ids = [item.id for item in item_collection]
+
+    assert expected_ids == actual_ids
+
+
+def test_item_collection_get_item_by_index(items: list[Item]) -> None:
+    expected_id = items[0].id
+    item_collection = pystac.ItemCollection(items=items)
+
+    assert item_collection[0].id == expected_id
+
+
+def test_item_collection_contains() -> None:
+    item = pystac.Item.from_file(SIMPLE_ITEM)
+    item_collection = pystac.ItemCollection(items=[item], clone_items=False)
 
-class TestItemCollection(unittest.TestCase):
-    SIMPLE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/simple-item.json")
-    CORE_ITEM = TestCases.get_path("data-files/examples/1.0.0-RC1/core-item.json")
-    EXTENDED_ITEM = TestCases.get_path(
-        "data-files/examples/1.0.0-RC1/extended-item.json"
+    assert item in item_collection
+
+
+def test_item_collection_extra_fields(items: list[Item]) -> None:
+    item_collection = pystac.ItemCollection(
+        items=items, extra_fields={"custom_field": "My value"}
     )
 
-    ITEM_COLLECTION = TestCases.get_path(
-        "data-files/item-collection/sample-item-collection.json"
+    assert item_collection.extra_fields.get("custom_field") == "My value"
+
+
+def test_item_collection_to_dict(items: list[Item]) -> None:
+    item_collection = pystac.ItemCollection(
+        items=items, extra_fields={"custom_field": "My value"}
     )
 
-    def setUp(self) -> None:
-        self.maxDiff = None
-        with open(self.ITEM_COLLECTION) as src:
-            self.item_collection_dict = json.load(src)
-        self.items = [
-            pystac.Item.from_dict(f) for f in self.item_collection_dict["features"]
-        ]
-        self.stac_io = pystac.StacIO.default()
+    d = item_collection.to_dict()
 
-    def test_item_collection_length(self) -> None:
-        item_collection = pystac.ItemCollection(items=self.items)
+    assert len(d["features"]) == len(items)
+    assert d.get("custom_field") == "My value"
 
-        self.assertEqual(len(item_collection), len(self.items))
 
-    def test_item_collection_iter(self) -> None:
-        expected_ids = [item.id for item in self.items]
-        item_collection = pystac.ItemCollection(items=self.items)
+def test_item_collection_from_dict(items: list[Item]) -> None:
+    features = [item.to_dict(transform_hrefs=False) for item in items]
+    d = {
+        "type": "FeatureCollection",
+        "features": features,
+        "custom_field": "My value",
+    }
+    item_collection = pystac.ItemCollection.from_dict(d)
+    expected = len(features)
+    assert expected == len(item_collection.items)
+    assert item_collection.extra_fields.get("custom_field") == "My value"
 
-        actual_ids = [item.id for item in item_collection]
 
-        self.assertListEqual(expected_ids, actual_ids)
+def test_clone_item_collection() -> None:
+    item_collection_1 = pystac.ItemCollection.from_file(ITEM_COLLECTION)
+    item_collection_2 = item_collection_1.clone()
 
-    def test_item_collection_get_item_by_index(self) -> None:
-        expected_id = self.items[0].id
-        item_collection = pystac.ItemCollection(items=self.items)
+    item_ids_1 = [item.id for item in item_collection_1]
+    item_ids_2 = [item.id for item in item_collection_2]
 
-        self.assertEqual(item_collection[0].id, expected_id)
+    # All items from the original collection should be in the clone...
+    assert item_ids_1 == item_ids_2
+    # ... but they should not be the same objects
+    assert item_collection_1[0] is not item_collection_2[0]
 
-    def test_item_collection_contains(self) -> None:
-        item = pystac.Item.from_file(self.SIMPLE_ITEM)
-        item_collection = pystac.ItemCollection(items=[item], clone_items=False)
 
-        self.assertIn(item, item_collection)
+def test_raise_error_for_invalid_object(stac_io: StacIO) -> None:
+    item_dict = stac_io.read_json(SIMPLE_ITEM)
 
-    def test_item_collection_extra_fields(self) -> None:
-        item_collection = pystac.ItemCollection(
-            items=self.items, extra_fields={"custom_field": "My value"}
-        )
+    with pytest.raises(pystac.STACTypeError):
+        _ = pystac.ItemCollection.from_dict(item_dict)
 
-        self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value")
 
-    def test_item_collection_to_dict(self) -> None:
-        item_collection = pystac.ItemCollection(
-            items=self.items, extra_fields={"custom_field": "My value"}
+def test_from_relative_path() -> None:
+    _ = pystac.ItemCollection.from_file(
+        relpath(
+            TestCases.get_path("data-files/item-collection/sample-item-collection.json")
         )
+    )
 
-        d = item_collection.to_dict()
-
-        self.assertEqual(len(d["features"]), len(self.items))
-        self.assertEqual(d.get("custom_field"), "My value")
-
-    def test_item_collection_from_dict(self) -> None:
-        features = [item.to_dict(transform_hrefs=False) for item in self.items]
-        d = {
-            "type": "FeatureCollection",
-            "features": features,
-            "custom_field": "My value",
-        }
-        item_collection = pystac.ItemCollection.from_dict(d)
-        expected = len(features)
-        self.assertEqual(expected, len(item_collection.items))
-        self.assertEqual(item_collection.extra_fields.get("custom_field"), "My value")
-
-    def test_clone_item_collection(self) -> None:
-        item_collection_1 = pystac.ItemCollection.from_file(self.ITEM_COLLECTION)
-        item_collection_2 = item_collection_1.clone()
-
-        item_ids_1 = [item.id for item in item_collection_1]
-        item_ids_2 = [item.id for item in item_collection_2]
-
-        # All items from the original collection should be in the clone...
-        self.assertListEqual(item_ids_1, item_ids_2)
-        # ... but they should not be the same objects
-        self.assertIsNot(item_collection_1[0], item_collection_2[0])
-
-    def test_raise_error_for_invalid_object(self) -> None:
-        item_dict = self.stac_io.read_json(self.SIMPLE_ITEM)
-
-        with self.assertRaises(pystac.STACTypeError):
-            _ = pystac.ItemCollection.from_dict(item_dict)
-
-    def test_from_relative_path(self) -> None:
-        _ = pystac.ItemCollection.from_file(
-            relpath(
-                TestCases.get_path(
-                    "data-files/item-collection/sample-item-collection.json"
-                )
-            )
-        )
 
-    def test_from_list_of_dicts(self) -> None:
-        item_dict = self.stac_io.read_json(self.SIMPLE_ITEM)
-        item_collection = pystac.ItemCollection(items=[item_dict], clone_items=True)
+def test_from_list_of_dicts(stac_io: StacIO) -> None:
+    item_dict = stac_io.read_json(SIMPLE_ITEM)
+    item_collection = pystac.ItemCollection(items=[item_dict], clone_items=True)
 
-        self.assertEqual(item_collection[0].id, item_dict.get("id"))
+    assert item_collection[0].id == item_dict.get("id")
 
-    def test_add_item_collections(self) -> None:
-        item_1 = pystac.Item.from_file(self.SIMPLE_ITEM)
-        item_2 = pystac.Item.from_file(self.EXTENDED_ITEM)
-        item_3 = pystac.Item.from_file(self.CORE_ITEM)
 
-        item_collection_1 = pystac.ItemCollection(items=[item_1, item_2])
-        item_collection_2 = pystac.ItemCollection(items=[item_2, item_3])
+def test_add_item_collections() -> None:
+    item_1 = pystac.Item.from_file(SIMPLE_ITEM)
+    item_2 = pystac.Item.from_file(EXTENDED_ITEM)
+    item_3 = pystac.Item.from_file(CORE_ITEM)
 
-        combined = item_collection_1 + item_collection_2
+    item_collection_1 = pystac.ItemCollection(items=[item_1, item_2])
+    item_collection_2 = pystac.ItemCollection(items=[item_2, item_3])
 
-        self.assertEqual(len(combined), 4)
+    combined = item_collection_1 + item_collection_2
 
-    def test_add_other_raises_error(self) -> None:
-        item_collection = pystac.ItemCollection.from_file(self.ITEM_COLLECTION)
+    assert len(combined) == 4
 
-        with self.assertRaises(TypeError):
-            _ = item_collection + 2
 
-    def test_identify_0_8_itemcollection_type(self) -> None:
-        itemcollection_path = TestCases.get_path(
-            "data-files/examples/0.8.1/item-spec/"
-            "examples/itemcollection-sample-full.json"
-        )
-        itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path)
+def test_add_other_raises_error() -> None:
+    item_collection = pystac.ItemCollection.from_file(ITEM_COLLECTION)
 
-        self.assertTrue(
-            pystac.ItemCollection.is_item_collection(itemcollection_dict),
-            msg="Did not correctly identify valid STAC 0.8 ItemCollection.",
-        )
+    with pytest.raises(TypeError):
+        _ = item_collection + 2
 
-    def test_identify_0_9_itemcollection(self) -> None:
-        itemcollection_path = TestCases.get_path(
-            "data-files/examples/0.9.0/item-spec/"
-            "examples/itemcollection-sample-full.json"
-        )
-        itemcollection_dict = pystac.StacIO.default().read_json(itemcollection_path)
 
-        self.assertTrue(
-            pystac.ItemCollection.is_item_collection(itemcollection_dict),
-            msg="Did not correctly identify valid STAC 0.9 ItemCollection.",
-        )
+def test_identify_0_8_itemcollection_type(stac_io: StacIO) -> None:
+    itemcollection_path = TestCases.get_path(
+        "data-files/examples/0.8.1/item-spec/"
+        "examples/itemcollection-sample-full.json"
+    )
+    itemcollection_dict = stac_io.read_json(itemcollection_path)
+
+    assert pystac.ItemCollection.is_item_collection(
+        itemcollection_dict
+    ), "Did not correctly identify valid STAC 0.8 ItemCollection."
+
+
+def test_identify_0_9_itemcollection(stac_io: StacIO) -> None:
+    itemcollection_path = TestCases.get_path(
+        "data-files/examples/0.9.0/item-spec/"
+        "examples/itemcollection-sample-full.json"
+    )
+    itemcollection_dict = stac_io.read_json(itemcollection_path)
+
+    assert pystac.ItemCollection.is_item_collection(
+        itemcollection_dict
+    ), "Did not correctly identify valid STAC 0.9 ItemCollection."
+
+
+def test_from_dict_preserves_dict(item_collection_dict: dict[str, Any]) -> None:
+    param_dict = deepcopy(item_collection_dict)
+
+    # test that the parameter is preserved
+    _ = ItemCollection.from_dict(param_dict)
+    assert param_dict == item_collection_dict
 
-    def test_from_dict_preserves_dict(self) -> None:
-        param_dict = deepcopy(self.item_collection_dict)
+    # assert that the parameter is preserved regardless of
+    # preserve_dict
+    _ = ItemCollection.from_dict(param_dict, preserve_dict=False)
+    assert param_dict == item_collection_dict
 
-        # test that the parameter is preserved
-        _ = ItemCollection.from_dict(param_dict)
-        self.assertEqual(param_dict, self.item_collection_dict)
 
-        # assert that the parameter is preserved regardless of
-        # preserve_dict
-        _ = ItemCollection.from_dict(param_dict, preserve_dict=False)
-        self.assertEqual(param_dict, self.item_collection_dict)
+def test_from_dict_sets_root(item_collection_dict: dict[str, Any]) -> None:
+    param_dict = deepcopy(item_collection_dict)
+    catalog = pystac.Catalog(id="test", description="test desc")
+    item_collection = ItemCollection.from_dict(param_dict, root=catalog)
+    for item in item_collection.items:
+        assert item.get_root() == catalog
 
-    def test_from_dict_sets_root(self) -> None:
-        param_dict = deepcopy(self.item_collection_dict)
-        catalog = pystac.Catalog(id="test", description="test desc")
-        item_collection = ItemCollection.from_dict(param_dict, root=catalog)
-        for item in item_collection.items:
-            self.assertEqual(item.get_root(), catalog)
 
-    def test_to_dict_does_not_read_root_link_of_items(self) -> None:
-        with MockDefaultStacIO() as mock_stac_io:
-            item_collection = pystac.ItemCollection.from_file(self.ITEM_COLLECTION)
+def test_to_dict_does_not_read_root_link_of_items() -> None:
+    with MockDefaultStacIO() as mock_stac_io:
+        item_collection = pystac.ItemCollection.from_file(ITEM_COLLECTION)
 
-            item_collection.to_dict()
+        item_collection.to_dict()
 
-            self.assertEqual(mock_stac_io.mock.read_text.call_count, 1)
+        assert mock_stac_io.mock.read_text.call_count == 1


=====================================
tests/test_version.py
=====================================
@@ -1,25 +1,31 @@
 import os
-import unittest
+from collections.abc import Generator
 from unittest.mock import patch
 
+import pytest
+
 import pystac
 from tests.utils import TestCases
 
 
-class VersionTest(unittest.TestCase):
-    def setUp(self) -> None:
-        pystac.version.STACVersion._override_version = None
-
-    def test_override_stac_version_with_environ(self) -> None:
-        override_version = "1.0.0-gamma.2"
-        with patch.dict(os.environ, {"PYSTAC_STAC_VERSION_OVERRIDE": override_version}):
-            cat = TestCases.case_1()
-            d = cat.to_dict()
-        self.assertEqual(d["stac_version"], override_version)
-
-    def test_override_stac_version_with_call(self) -> None:
-        override_version = "1.0.0-delta.2"
-        pystac.set_stac_version(override_version)
+def test_override_stac_version_with_environ() -> None:
+    override_version = "1.0.0-gamma.2"
+    with patch.dict(os.environ, {"PYSTAC_STAC_VERSION_OVERRIDE": override_version}):
         cat = TestCases.case_1()
         d = cat.to_dict()
-        self.assertEqual(d["stac_version"], override_version)
+    assert d["stac_version"] == override_version
+
+
+ at pytest.fixture
+def override_pystac_version() -> Generator[str]:
+    stac_version = pystac.get_stac_version()
+    override_version = "1.0.0-delta.2"
+    pystac.set_stac_version(override_version)
+    yield override_version
+    pystac.set_stac_version(stac_version)
+
+
+def test_override_stac_version_with_call(override_pystac_version: str) -> None:
+    cat = TestCases.case_1()
+    d = cat.to_dict()
+    assert d["stac_version"] == override_pystac_version


=====================================
uv.lock
=====================================
@@ -1836,7 +1836,7 @@ wheels = [
 
 [[package]]
 name = "pystac"
-version = "1.11.0"
+version = "1.12.1"
 source = { editable = "." }
 dependencies = [
     { name = "python-dateutil" },



View it on GitLab: https://salsa.debian.org/debian-gis-team/pystac/-/commit/768dc38c30409bca26c6383ab9b04df4c2d3853c

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pystac/-/commit/768dc38c30409bca26c6383ab9b04df4c2d3853c
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20250222/37bd4afb/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list