New Upstream Release - python-zipp

Ready changes

Summary

Merged new upstream version: 3.16.2 (was: 3.11.0).

Resulting package

Built on 2023-08-19T03:26 (took 4m42s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-zipp

Lintian Result

Diff

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0287948
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,9 @@
+[run]
+omit =
+	# leading `*/` for pytest-dev/pytest-cov#456
+	*/.tox/*
+disable_warnings =
+	couldnt-parse
+
+[report]
+show_missing = True
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..304196f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+indent_size = 4
+insert_final_newline = true
+end_of_line = lf
+
+[*.py]
+indent_style = space
+max_line_length = 88
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.rst]
+indent_style = space
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 790c109..0000000
--- a/.flake8
+++ /dev/null
@@ -1,9 +0,0 @@
-[flake8]
-max-line-length = 88
-ignore =
-	# W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513
-	W503
-	# W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545
-	W504
-	# Black creates whitespace before colon
-	E203
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..a387ba6
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+tidelift: pypi/zipp
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..89ff339
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "daily"
+    allow:
+      - dependency-type: "all"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..886651d
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,133 @@
+name: tests
+
+on: [push, pull_request]
+
+permissions:
+  contents: read
+
+env:
+  # Environment variables to support color support (jaraco/skeleton#66):
+  # Request colored output from CLI tools supporting it. Different tools
+  # interpret the value differently. For some, just being set is sufficient.
+  # For others, it must be a non-zero integer. For yet others, being set
+  # to a non-empty value is sufficient. For tox, it must be one of
+  # <blank>, 0, 1, false, no, off, on, true, yes. The only enabling value
+  # in common is "1".
+  FORCE_COLOR: 1
+  # MyPy's color enforcement (must be a non-zero number)
+  MYPY_FORCE_COLOR: -42
+  # Recognized by the `py` package, dependency of `pytest` (must be "1")
+  PY_COLORS: 1
+  # Make tox-wrapped tools see color requests
+  TOX_TESTENV_PASSENV: >-
+    FORCE_COLOR
+    MYPY_FORCE_COLOR
+    NO_COLOR
+    PY_COLORS
+    PYTEST_THEME
+    PYTEST_THEME_MODE
+
+  # Suppress noisy pip warnings
+  PIP_DISABLE_PIP_VERSION_CHECK: 'true'
+  PIP_NO_PYTHON_VERSION_WARNING: 'true'
+  PIP_NO_WARN_SCRIPT_LOCATION: 'true'
+
+  # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream
+  # Must be "1".
+  TOX_PARALLEL_NO_SPINNER: 1
+
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        python:
+        - "3.8"
+        - "3.11"
+        - "3.12"
+        platform:
+        - ubuntu-latest
+        - macos-latest
+        - windows-latest
+        include:
+        - python: "3.9"
+          platform: ubuntu-latest
+        - python: "3.10"
+          platform: ubuntu-latest
+        - python: pypy3.9
+          platform: ubuntu-latest
+        exclude:
+        # disabled for numpy/numpy#23808
+        - python: "3.12"
+          platform: windows-latest
+    runs-on: ${{ matrix.platform }}
+    continue-on-error: ${{ matrix.python == '3.12' }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          # fetch all branches and tags (to get tags for versioning)
+          # ref actions/checkout#448
+          fetch-depth: 0
+
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python }}
+          allow-prereleases: true
+      - name: Install tox
+        run: |
+          python -m pip install tox
+      - name: Run
+        run: tox
+
+  docs:
+    runs-on: ubuntu-latest
+    env:
+      TOXENV: docs
+    steps:
+      - uses: actions/checkout@v3
+      - name: Setup Python
+        uses: actions/setup-python@v4
+      - name: Install tox
+        run: |
+          python -m pip install tox
+      - name: Run
+        run: tox
+
+  check:  # This job does nothing and is only used for the branch protection
+    if: always()
+
+    needs:
+    - test
+    - docs
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Decide whether the needed jobs succeeded or failed
+      uses: re-actors/alls-green@release/v1
+      with:
+        jobs: ${{ toJSON(needs) }}
+
+  release:
+    permissions:
+      contents: write
+    needs:
+    - check
+    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+      - name: Install tox
+        run: |
+          python -m pip install tox
+      - name: Run
+        run: tox -e release
+        env:
+          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 922d942..af50201 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
 repos:
-- repo: https://github.com/ambv/black
-  rev: 18.9b0
+- repo: https://github.com/psf/black
+  rev: 22.6.0
   hooks:
   - id: black
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..053c728
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,12 @@
+version: 2
+python:
+  install:
+  - path: .
+    extra_requirements:
+      - docs
+
+# required boilerplate readthedocs/readthedocs.org#10401
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3"
diff --git a/.readthedocs.yml b/.readthedocs.yml
deleted file mode 100644
index 8ae4468..0000000
--- a/.readthedocs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-python:
-  version: 3
-  extra_requirements:
-    - docs
-  pip_install: true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ac89877..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-dist: xenial
-language: python
-
-python:
-- 2.7
-- 3.5
-- &latest_py3 3.7
-
-jobs:
-  fast_finish: true
-  include:
-  - stage: deploy
-    if: tag IS present
-    python: *latest_py3
-    before_script: skip
-    script: tox -e release
-
-cache: pip
-
-install:
-- pip install tox tox-venv
-
-before_script:
-  # Disable IPv6. Ref travis-ci/travis-ci#8361
-  - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then
-      sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
-    fi
-script: tox
diff --git a/CHANGES.rst b/CHANGES.rst
deleted file mode 100644
index bcb6a2d..0000000
--- a/CHANGES.rst
+++ /dev/null
@@ -1,74 +0,0 @@
-v1.0.0
-======
-
-Re-release of 0.6 to correspond with release as found in
-Python 3.8.
-
-v0.6.0
-======
-
-#12: When adding implicit dirs, ensure that ancestral directories
-are added and that duplicates are excluded.
-
-The library now relies on
-`more_itertools <https://pypi.org/project/more_itertools>`_.
-
-v0.5.2
-======
-
-#7: Parent of a directory now actually returns the parent.
-
-v0.5.1
-======
-
-Declared package as backport.
-
-v0.5.0
-======
-
-Add ``.joinpath()`` method and ``.parent`` property.
-
-Now a backport release of the ``zipfile.Path`` class.
-
-v0.4.0
-======
-
-#4: Add support for zip files with implied directories.
-
-v0.3.3
-======
-
-#3: Fix issue where ``.name`` on a directory was empty.
-
-v0.3.2
-======
-
-#2: Fix TypeError on Python 2.7 when classic division is used.
-
-v0.3.1
-======
-
-#1: Fix TypeError on Python 3.5 when joining to a path-like object.
-
-v0.3.0
-======
-
-Add support for constructing a ``zipp.Path`` from any path-like
-object.
-
-``zipp.Path`` is now a new-style class on Python 2.7.
-
-v0.2.1
-======
-
-Fix issue with ``__str__``.
-
-v0.2.0
-======
-
-Drop reliance on future-fstrings.
-
-v0.1.0
-======
-
-Initial release with basic functionality.
diff --git a/LICENSE b/LICENSE
index 5e795a6..1bb5a44 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,17 @@
-Copyright Jason R. Coombs
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
 
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/NEWS.rst b/NEWS.rst
new file mode 100644
index 0000000..9296482
--- /dev/null
+++ b/NEWS.rst
@@ -0,0 +1,303 @@
+v3.16.2
+=======
+
+Bugfixes
+--------
+
+- In ``Path.match``, Windows path separators are no longer honored. The fact that they were was incidental and never supported. (#92)
+- Fixed name/suffix/suffixes/stem operations when no filename is present and the Path is not at the root of the zipfile. (#96)
+- Reworked glob utilizing the namelist directly. (#101)
+
+
+v3.16.1
+=======
+
+Bugfixes
+--------
+
+- Replaced the ``fnmatch.translate`` with a fresh glob-to-regex translator for more correct matching behavior. (#98)
+
+
+v3.16.0
+=======
+
+Features
+--------
+
+- Require Python 3.8 or later.
+
+
+v3.15.0
+=======
+
+* gh-102209: ``test_implied_dirs_performance`` now tests
+  measures the time complexity experimentally.
+
+v3.14.0
+=======
+
+* Minor cleanup in tests, including #93.
+
+v3.13.0
+=======
+
+* In tests, add a fallback when ``func_timeout`` isn't available.
+
+v3.12.1
+=======
+
+* gh-101566: In ``CompleteDirs``, override ``ZipFile.getinfo``
+  to supply a ``ZipInfo`` for implied dirs.
+
+v3.12.0
+=======
+
+* gh-101144: Honor ``encoding`` as positional parameter
+  to ``Path.open()`` and ``Path.read_text()``.
+
+v3.11.0
+=======
+
+* #85: Added support for new methods on ``Path``:
+
+  - ``match``
+  - ``glob`` and ``rglob``
+  - ``relative_to``
+  - ``is_symlink``
+
+v3.10.0
+=======
+
+* ``zipp`` is now a package.
+
+v3.9.1
+======
+
+* Removed 'print' expression in test_pickle.
+
+* bpo-43651: Apply ``io.text_encoding`` on Python 3.10 and later.
+
+v3.9.0
+======
+
+* #81: ``Path`` objects are now pickleable if they've been
+  constructed from pickleable objects. Any restored objects
+  will re-construct the zip file with the original arguments.
+
+v3.8.1
+======
+
+Refreshed packaging.
+
+Enrolled with Tidelift.
+
+v3.8.0
+======
+
+Removed compatibility code.
+
+v3.7.0
+======
+
+Require Python 3.7 or later.
+
+v3.6.0
+======
+
+#78: Only ``Path`` is exposed in the public API.
+
+v3.5.1
+======
+
+#77: Remove news file intended only for CPython.
+
+v3.5.0
+======
+
+#74 and bpo-44095: Added ``.suffix``, ``.suffixes``,
+and ``.stem`` properties.
+
+v3.4.2
+======
+
+Refresh package metadata.
+
+v3.4.1
+======
+
+Refresh packaging.
+
+v3.4.0
+======
+
+#68 and bpo-42090: ``Path.joinpath`` now takes arbitrary
+positional arguments and no longer accepts ``add`` as a
+keyword argument.
+
+v3.3.2
+======
+
+Updated project metadata including badges.
+
+v3.3.1
+======
+
+bpo-42043: Add tests capturing subclassing requirements.
+
+v3.3.0
+======
+
+#9: ``Path`` objects now expose a ``.filename`` attribute
+and rely on that to resolve ``.name`` and ``.parent`` when
+the ``Path`` object is at the root of the zipfile.
+
+v3.2.0
+======
+
+#57 and bpo-40564: Mutate the passed ZipFile object
+type instead of making a copy. Prevents issues when
+both the local copy and the caller's copy attempt to
+close the same file handle.
+
+#56 and bpo-41035: ``Path._next`` now honors
+subclasses.
+
+#55: ``Path.is_file()`` now returns False for non-existent names.
+
+v3.1.0
+======
+
+#47: ``.open`` now raises ``FileNotFoundError`` and
+``IsADirectoryError`` when appropriate.
+
+v3.0.0
+======
+
+#44: Merge with v1.2.0.
+
+v1.2.0
+======
+
+#44: ``zipp.Path.open()`` now supports a compatible signature
+as ``pathlib.Path.open()``, accepting text (default) or binary
+modes and soliciting keyword parameters passed through to
+``io.TextIOWrapper`` (encoding, newline, etc). The stream is
+opened in text-mode by default now. ``open`` no
+longer accepts ``pwd`` as a positional argument and does not
+accept the ``force_zip64`` parameter at all. This change is
+a backward-incompatible change for that single function.
+
+v2.2.1
+======
+
+#43: Merge with v1.1.1.
+
+v1.1.1
+======
+
+#43: Restored performance of implicit dir computation.
+
+v2.2.0
+======
+
+#36: Rebuild package with minimum Python version declared both
+in package metadata and in the python tag.
+
+v2.1.0
+======
+
+#32: Merge with v1.1.0.
+
+v1.1.0
+======
+
+#32: For read-only zip files, complexity of ``.exists`` and
+``joinpath`` is now constant time instead of ``O(n)``, preventing
+quadratic time in common use-cases and rendering large
+zip files unusable for Path. Big thanks to Benjy Weinberger
+for the bug report and contributed fix (#33).
+
+v2.0.1
+======
+
+#30: Corrected version inference (from jaraco/skeleton#12).
+
+v2.0.0
+======
+
+Require Python 3.6 or later.
+
+v1.0.0
+======
+
+Re-release of 0.6 to correspond with release as found in
+Python 3.8.
+
+v0.6.0
+======
+
+#12: When adding implicit dirs, ensure that ancestral directories
+are added and that duplicates are excluded.
+
+The library now relies on
+`more_itertools <https://pypi.org/project/more_itertools>`_.
+
+v0.5.2
+======
+
+#7: Parent of a directory now actually returns the parent.
+
+v0.5.1
+======
+
+Declared package as backport.
+
+v0.5.0
+======
+
+Add ``.joinpath()`` method and ``.parent`` property.
+
+Now a backport release of the ``zipfile.Path`` class.
+
+v0.4.0
+======
+
+#4: Add support for zip files with implied directories.
+
+v0.3.3
+======
+
+#3: Fix issue where ``.name`` on a directory was empty.
+
+v0.3.2
+======
+
+#2: Fix TypeError on Python 2.7 when classic division is used.
+
+v0.3.1
+======
+
+#1: Fix TypeError on Python 3.5 when joining to a path-like object.
+
+v0.3.0
+======
+
+Add support for constructing a ``zipp.Path`` from any path-like
+object.
+
+``zipp.Path`` is now a new-style class on Python 2.7.
+
+v0.2.1
+======
+
+Fix issue with ``__str__``.
+
+v0.2.0
+======
+
+Drop reliance on future-fstrings.
+
+v0.1.0
+======
+
+Initial release with basic functionality.
diff --git a/PKG-INFO b/PKG-INFO
index 1847ed3..9ccf2b5 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,39 +1,85 @@
 Metadata-Version: 2.1
 Name: zipp
-Version: 1.0.0
+Version: 3.16.2
 Summary: Backport of pathlib-compatible object wrapper for zip files
 Home-page: https://github.com/jaraco/zipp
 Author: Jason R. Coombs
 Author-email: jaraco@jaraco.com
-License: UNKNOWN
-Description: .. image:: https://img.shields.io/pypi/v/zipp.svg
-           :target: https://pypi.org/project/zipp
-        
-        .. image:: https://img.shields.io/pypi/pyversions/zipp.svg
-        
-        .. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
-           :target: https://travis-ci.org/jaraco/zipp
-        
-        .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-           :target: https://github.com/ambv/black
-           :alt: Code style: Black
-        
-        .. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
-           :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
-        
-        .. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
-        ..    :target: https://zipp.readthedocs.io/en/latest/?badge=latest
-        
-        
-        A pathlib-compatible Zipfile object wrapper. A backport of the
-        `Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
-        
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Requires-Python: >=2.7
+Classifier: Programming Language :: Python :: 3 :: Only
+Requires-Python: >=3.8
 Provides-Extra: testing
 Provides-Extra: docs
+License-File: LICENSE
+
+.. image:: https://img.shields.io/pypi/v/zipp.svg
+   :target: https://pypi.org/project/zipp
+
+.. image:: https://img.shields.io/pypi/pyversions/zipp.svg
+
+.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg
+   :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22
+   :alt: tests
+
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+    :target: https://github.com/astral-sh/ruff
+    :alt: Ruff
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: Black
+
+.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest
+..    :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2023-informational
+   :target: https://blog.jaraco.com/skeleton
+
+.. image:: https://tidelift.com/badges/package/pypi/zipp
+   :target: https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=readme
+
+
+A pathlib-compatible Zipfile object wrapper. Official backport of the standard library
+`Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
+
+
+Compatibility
+=============
+
+New features are introduced in this third-party library and later merged
+into CPython. The following table indicates which versions of this library
+were contributed to different versions in the standard library:
+
+.. list-table::
+   :header-rows: 1
+
+   * - zipp
+     - stdlib
+   * - 3.15
+     - 3.12
+   * - 3.5
+     - 3.11
+   * - 3.2
+     - 3.10
+   * - 3.3 ??
+     - 3.9
+   * - 1.0
+     - 3.8
+
+
+Usage
+=====
+
+Use ``zipp.Path`` in place of ``zipfile.Path`` on any Python.
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more <https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=referral&utm_campaign=github>`_.
diff --git a/README.rst b/README.rst
index ce128a3..564d152 100644
--- a/README.rst
+++ b/README.rst
@@ -3,19 +3,66 @@
 
 .. image:: https://img.shields.io/pypi/pyversions/zipp.svg
 
-.. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
-   :target: https://travis-ci.org/jaraco/zipp
+.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg
+   :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22
+   :alt: tests
+
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+    :target: https://github.com/astral-sh/ruff
+    :alt: Ruff
 
 .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-   :target: https://github.com/ambv/black
+   :target: https://github.com/psf/black
    :alt: Code style: Black
 
-.. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
-   :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
+.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest
+..    :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2023-informational
+   :target: https://blog.jaraco.com/skeleton
 
-.. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
-..    :target: https://zipp.readthedocs.io/en/latest/?badge=latest
+.. image:: https://tidelift.com/badges/package/pypi/zipp
+   :target: https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=readme
 
 
-A pathlib-compatible Zipfile object wrapper. A backport of the
+A pathlib-compatible Zipfile object wrapper. Official backport of the standard library
 `Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
+
+
+Compatibility
+=============
+
+New features are introduced in this third-party library and later merged
+into CPython. The following table indicates which versions of this library
+were contributed to different versions in the standard library:
+
+.. list-table::
+   :header-rows: 1
+
+   * - zipp
+     - stdlib
+   * - 3.15
+     - 3.12
+   * - 3.5
+     - 3.11
+   * - 3.2
+     - 3.10
+   * - 3.3 ??
+     - 3.9
+   * - 1.0
+     - 3.8
+
+
+Usage
+=====
+
+Use ``zipp.Path`` in place of ``zipfile.Path`` on any Python.
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more <https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=referral&utm_campaign=github>`_.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..54f99ac
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,3 @@
+# Security Contact
+
+To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index f35aa27..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-environment:
-
-  APPVEYOR: true
-
-  matrix:
-    - PYTHON: "C:\\Python36-x64"
-    - PYTHON: "C:\\Python27-x64"
-
-install:
-  # symlink python from a directory with a space
-  - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%"
-  - "SET PYTHON=\"C:\\Program Files\\Python\""
-  - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
-
-build: off
-
-cache:
-  - '%LOCALAPPDATA%\pip\Cache'
-
-test_script:
-  - "python -m pip install -U tox tox-venv virtualenv"
-  - "tox"
-
-version: '{build}'
diff --git a/conftest.py b/conftest.py
index e69de29..5180b34 100644
--- a/conftest.py
+++ b/conftest.py
@@ -0,0 +1,13 @@
+import builtins
+import sys
+
+
+def pytest_configure():
+    add_future_flags()
+
+
+def add_future_flags():  # pragma: no cover
+    if sys.version_info > (3, 10):
+        return
+
+    builtins.EncodingWarning = type('EncodingWarning', (Warning,), {})
diff --git a/debian/changelog b/debian/changelog
index 379db57..0dce597 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-zipp (3.16.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 19 Aug 2023 03:22:41 -0000
+
 python-zipp (1.0.0-6) unstable; urgency=medium
 
   * Team upload.
diff --git a/docs/conf.py b/docs/conf.py
index 41b5355..e25a514 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,12 +1,15 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker']
+extensions = [
+    'sphinx.ext.autodoc',
+    'jaraco.packaging.sphinx',
+]
 
 master_doc = "index"
+html_theme = "furo"
 
+# Link dates and other references in the changelog
+extensions += ['rst.linker']
 link_files = {
-    '../CHANGES.rst': dict(
+    '../NEWS.rst': dict(
         using=dict(GH='https://github.com'),
         replace=[
             dict(
@@ -14,13 +17,36 @@ link_files = {
                 url='{package_url}/issues/{issue}',
             ),
             dict(
-                pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n',
+                pattern=r'(?m:^((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n)',
                 with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n',
             ),
             dict(
                 pattern=r'PEP[- ](?P<pep_number>\d+)',
-                url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/',
+                url='https://peps.python.org/pep-{pep_number:0>4}/',
+            ),
+            dict(
+                pattern=r'(bpo-)(?P<bpo>\d+)',
+                url='http://bugs.python.org/issue{bpo}',
+            ),
+            dict(
+                pattern=r'(gh-)(?P<python_gh>\d+)',
+                url='http://bugs.python.org/issue{python_gh}',
             ),
         ],
     )
 }
+
+# Be strict about any broken references
+nitpicky = True
+
+# Include Python intersphinx mapping to prevent failures
+# jaraco/skeleton#51
+extensions += ['sphinx.ext.intersphinx']
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3', None),
+}
+
+# Preserve authored syntax for defaults
+autodoc_preserve_defaults = True
+
+extensions += ['jaraco.tidelift']
diff --git a/docs/history.rst b/docs/history.rst
index 8e21750..5bdc232 100644
--- a/docs/history.rst
+++ b/docs/history.rst
@@ -5,4 +5,4 @@
 History
 *******
 
-.. include:: ../CHANGES (links).rst
+.. include:: ../NEWS (links).rst
diff --git a/docs/index.rst b/docs/index.rst
index ff49bf9..d76eb3c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,5 +1,9 @@
-Welcome to zipp documentation!
-========================================
+Welcome to |project| documentation!
+===================================
+
+.. sidebar-links::
+   :home:
+   :pypi:
 
 .. toctree::
    :maxdepth: 1
@@ -7,11 +11,18 @@ Welcome to zipp documentation!
    history
 
 
+.. tidelift-referral-banner::
+
 .. automodule:: zipp
     :members:
     :undoc-members:
     :show-inheritance:
 
+.. automodule:: zipp.glob
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 
 Indices and tables
 ==================
@@ -19,4 +30,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..b6f9727
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,5 @@
+[mypy]
+ignore_missing_imports = True
+# required to support namespace packages
+# https://github.com/python/mypy/issues/14057
+explicit_package_bases = True
diff --git a/pyproject.toml b/pyproject.toml
index 3afc8c3..dce944d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,8 @@
 [build-system]
-requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"]
+requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"]
 build-backend = "setuptools.build_meta"
 
 [tool.black]
 skip-string-normalization = true
+
+[tool.setuptools_scm]
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..d9a15ed
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,27 @@
+[pytest]
+norecursedirs=dist build .tox .eggs
+addopts=--doctest-modules
+filterwarnings=
+	## upstream
+
+	# Ensure ResourceWarnings are emitted
+	default::ResourceWarning
+
+	# shopkeep/pytest-black#55
+	ignore:<class 'pytest_black.BlackItem'> is not using a cooperative constructor:pytest.PytestDeprecationWarning
+	ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning
+	ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning
+
+	# shopkeep/pytest-black#67
+	ignore:'encoding' argument not specified::pytest_black
+
+	# realpython/pytest-mypy#152
+	ignore:'encoding' argument not specified::pytest_mypy
+
+	# python/cpython#100750
+	ignore:'encoding' argument not specified::platform
+
+	# pypa/build#615
+	ignore:'encoding' argument not specified::build.env
+
+	## end upstream
diff --git a/setup.cfg b/setup.cfg
index 0c583d8..1da5c66 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,4 @@
-[bdist_wheel]
-universal = 1
-
 [metadata]
-license_file = LICENSE
 name = zipp
 author = Jason R. Coombs
 author_email = jaraco@jaraco.com
@@ -13,28 +9,47 @@ classifiers =
 	Development Status :: 5 - Production/Stable
 	Intended Audience :: Developers
 	License :: OSI Approved :: MIT License
-	Programming Language :: Python :: 2.7
 	Programming Language :: Python :: 3
+	Programming Language :: Python :: 3 :: Only
 
 [options]
-py_modules = zipp
-packages = find:
+packages = find_namespace:
 include_package_data = true
-python_requires = >=2.7
+python_requires = >=3.8
 install_requires = 
-	more_itertools
-setup_requires = setuptools_scm >= 1.15.0
+
+[options.packages.find]
+exclude = 
+	build*
+	dist*
+	docs*
+	tests*
 
 [options.extras_require]
 testing = 
+	pytest >= 6
+	pytest-checkdocs >= 2.4
+	pytest-black >= 0.3.7; \
+	python_implementation != "PyPy"
+	pytest-cov
+	pytest-mypy >= 0.9.1; \
+	python_implementation != "PyPy"
+	pytest-enabler >= 2.2
+	pytest-ruff
 	
-	pathlib2
-	contextlib2
-	unittest2
+	jaraco.itertools
+	jaraco.functools
+	more_itertools
+	big-O
+	pytest-ignore-flaky
 docs = 
-	sphinx
-	jaraco.packaging >= 3.2
+	sphinx >= 3.5
+	jaraco.packaging >= 9.3
 	rst.linker >= 1.9
+	furo
+	sphinx-lint
+	
+	jaraco.tidelift >= 1.4
 
 [options.entry_points]
 
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 827e955..0000000
--- a/setup.py
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-
-import setuptools
-
-if __name__ == "__main__":
-    setuptools.setup(use_scm_version=True)
diff --git a/skeleton.md b/skeleton.md
deleted file mode 100644
index 52b97f0..0000000
--- a/skeleton.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# Overview
-
-This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution.
-
-## An SCM Managed Approach
-
-While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices.
-
-It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter.
-
-The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects.
-
-Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton.
-
-# Usage
-
-## new projects
-
-To use skeleton for a new project, simply pull the skeleton into a new project:
-
-```
-$ git init my-new-project
-$ cd my-new-project
-$ git pull gh://jaraco/skeleton
-```
-
-Now customize the project to suit your individual project needs.
-
-## existing projects
-
-If you have an existing project, you can still incorporate the skeleton by merging it into the codebase.
-
-```
-$ git merge skeleton --allow-unrelated-histories
-```
-
-The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton.
-
-## Updating
-
-Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations.
-
-Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints.
-
-# Features
-
-The features/techniques employed by the skeleton include:
-
-- PEP 517/518 based build relying on setuptools as the build tool
-- setuptools declarative configuration using setup.cfg
-- tox for running tests
-- A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out
-- A CHANGES.rst file intended for publishing release notes about the project
-- Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier)
-
-## Packaging Conventions
-
-A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config).
-
-The setup.cfg file implements the following features:
-
-- Assumes universal wheel for release
-- Advertises the project's LICENSE file (MIT by default)
-- Reads the README.rst file into the long description
-- Some common Trove classifiers
-- Includes all packages discovered in the repo
-- Data files in the package are also included (not just Python files)
-- Declares the required Python versions
-- Declares install requirements (empty by default)
-- Declares setup requirements for legacy environments
-- Supplies two 'extras':
-  - testing: requirements for running tests
-  - docs: requirements for building docs
-  - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts
-- Placeholder for defining entry points
-
-Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things:
-
-- derive the project version from SCM tags
-- ensure that all files committed to the repo are automatically included in releases
-
-## Running Tests
-
-The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest).
-
-Other environments (invoked with `tox -e {name}`) supplied include:
-
-  - a `build-docs` environment to build the documentation
-  - a `release` environment to publish the package to PyPI
-
-A pytest.ini is included to define common options around running tests. In particular:
-
-- rely on default test discovery in the current directory
-- avoid recursing into common directories not containing tests
-- run doctests on modules and invoke flake8 tests
-- in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option.
-- filters out known warnings caused by libraries/functionality included by the skeleton
-
-Relies a .flake8 file to correct some default behaviors:
-
-- disable mutually incompatible rules W503 and W504
-- support for black format
-
-## Continuous Integration
-
-The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command.
-
-Features include:
-- test against Python 2 and 3
-- run on Ubuntu Xenial
-- correct for broken IPv6
-
-Also provided is a minimal template for running under Appveyor (Windows).
-
-### Continuous Deployments
-
-In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax):
-
-```
-TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD
-```
-
-## Building Documentation
-
-Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`.
-
-In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs.
-
-## Cutting releases
-
-By default, tagged commits are released through the continuous integration deploy stage.
-
-Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD:
-
-```
-TWINE_PASSWORD={token} tox -e release
-```
diff --git a/test_zipp.py b/test_zipp.py
deleted file mode 100644
index e4c77a2..0000000
--- a/test_zipp.py
+++ /dev/null
@@ -1,198 +0,0 @@
-# coding: utf-8
-
-from __future__ import division, unicode_literals
-
-import io
-import zipfile
-import posixpath
-import contextlib
-import tempfile
-import shutil
-
-try:
-    import pathlib
-except ImportError:
-    import pathlib2 as pathlib
-
-if not hasattr(contextlib, 'ExitStack'):
-    import contextlib2
-    contextlib.ExitStack = contextlib2.ExitStack
-
-try:
-    import unittest
-
-    unittest.TestCase.subTest
-except AttributeError:
-    import unittest2 as unittest
-
-import zipp
-
-__metaclass__ = type
-consume = tuple
-
-
-def add_dirs(zf):
-    """
-    Given a writable zip file zf, inject directory entries for
-    any directories implied by the presence of children.
-    """
-    for name in zipp.Path._implied_dirs(zf.namelist()):
-        zf.writestr(name, b"")
-    return zf
-
-
-def build_alpharep_fixture():
-    """
-    Create a zip file with this structure:
-
-    .
-    ├── a.txt
-    ├── b
-    │   ├── c.txt
-    │   ├── d
-    │   │   └── e.txt
-    │   └── f.txt
-    └── g
-        └── h
-            └── i.txt
-
-    This fixture has the following key characteristics:
-
-    - a file at the root (a)
-    - a file two levels deep (b/d/e)
-    - multiple files in a directory (b/c, b/f)
-    - a directory containing only a directory (g/h)
-
-    "alpha" because it uses alphabet
-    "rep" because it's a representative example
-    """
-    data = io.BytesIO()
-    zf = zipfile.ZipFile(data, "w")
-    zf.writestr("a.txt", b"content of a")
-    zf.writestr("b/c.txt", b"content of c")
-    zf.writestr("b/d/e.txt", b"content of e")
-    zf.writestr("b/f.txt", b"content of f")
-    zf.writestr("g/h/i.txt", b"content of i")
-    zf.filename = "alpharep.zip"
-    return zf
-
-
-@contextlib.contextmanager
-def temp_dir():
-    tmpdir = tempfile.mkdtemp()
-    try:
-        yield pathlib.Path(tmpdir)
-    finally:
-        shutil.rmtree(tmpdir)
-
-
-class TestPath(unittest.TestCase):
-    def setUp(self):
-        self.fixtures = contextlib.ExitStack()
-        self.addCleanup(self.fixtures.close)
-
-    def zipfile_alpharep(self):
-        with self.subTest():
-            yield build_alpharep_fixture()
-        with self.subTest():
-            yield add_dirs(build_alpharep_fixture())
-
-    def zipfile_ondisk(self):
-        tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
-        for alpharep in self.zipfile_alpharep():
-            buffer = alpharep.fp
-            alpharep.close()
-            path = tmpdir / alpharep.filename
-            with path.open("wb") as strm:
-                strm.write(buffer.getvalue())
-            yield path
-
-    def test_iterdir_and_types(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            assert root.is_dir()
-            a, b, g = root.iterdir()
-            assert a.is_file()
-            assert b.is_dir()
-            assert g.is_dir()
-            c, f, d = b.iterdir()
-            assert c.is_file() and f.is_file()
-            e, = d.iterdir()
-            assert e.is_file()
-            h, = g.iterdir()
-            i, = h.iterdir()
-            assert i.is_file()
-
-    def test_open(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            a, b, g = root.iterdir()
-            with a.open() as strm:
-                data = strm.read()
-            assert data == b"content of a"
-
-    def test_read(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            a, b, g = root.iterdir()
-            assert a.read_text() == "content of a"
-            assert a.read_bytes() == b"content of a"
-
-    def test_joinpath(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            a = root.joinpath("a")
-            assert a.is_file()
-            e = root.joinpath("b").joinpath("d").joinpath("e.txt")
-            assert e.read_text() == "content of e"
-
-    def test_traverse_truediv(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            a = root / "a"
-            assert a.is_file()
-            e = root / "b" / "d" / "e.txt"
-            assert e.read_text() == "content of e"
-
-    def test_traverse_simplediv(self):
-        """
-        Disable the __future__.division when testing traversal.
-        """
-        for alpharep in self.zipfile_alpharep():
-            code = compile(
-                source="zipp.Path(alpharep) / 'a'",
-                filename="(test)",
-                mode="eval",
-                dont_inherit=True,
-            )
-            eval(code)
-
-    def test_pathlike_construction(self):
-        """
-        zipp.Path should be constructable from a path-like object
-        """
-        for zipfile_ondisk in self.zipfile_ondisk():
-            pathlike = pathlib.Path(str(zipfile_ondisk))
-            zipp.Path(pathlike)
-
-    def test_traverse_pathlike(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            root / pathlib.Path("a")
-
-    def test_parent(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            assert (root / 'a').parent.at == ''
-            assert (root / 'a' / 'b').parent.at == 'a/'
-
-    def test_dir_parent(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            assert (root / 'b').parent.at == ''
-            assert (root / 'b/').parent.at == ''
-
-    def test_missing_dir_parent(self):
-        for alpharep in self.zipfile_alpharep():
-            root = zipp.Path(alpharep)
-            assert (root / 'missing dir/').parent.at == ''
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/_support.py b/tests/_support.py
new file mode 100644
index 0000000..1afdf3b
--- /dev/null
+++ b/tests/_support.py
@@ -0,0 +1,9 @@
+import importlib
+import unittest
+
+
+def import_or_skip(name):
+    try:
+        return importlib.import_module(name)
+    except ImportError:  # pragma: no cover
+        raise unittest.SkipTest(f'Unable to import {name}')
diff --git a/tests/_test_params.py b/tests/_test_params.py
new file mode 100644
index 0000000..4e0b895
--- /dev/null
+++ b/tests/_test_params.py
@@ -0,0 +1,39 @@
+import types
+import functools
+
+from more_itertools import always_iterable
+
+
+def parameterize(names, value_groups):
+    """
+    Decorate a test method to run it as a set of subtests.
+
+    Modeled after pytest.parametrize.
+    """
+
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapped(self):
+            for values in value_groups:
+                resolved = map(Invoked.eval, always_iterable(values))
+                params = dict(zip(always_iterable(names), resolved))
+                with self.subTest(**params):
+                    func(self, **params)
+
+        return wrapped
+
+    return decorator
+
+
+class Invoked(types.SimpleNamespace):
+    """
+    Wrap a function to be invoked for each usage.
+    """
+
+    @classmethod
+    def wrap(cls, func):
+        return cls(func=func)
+
+    @classmethod
+    def eval(cls, cand):
+        return cand.func() if isinstance(cand, cls) else cand
diff --git a/tests/test_complexity.py b/tests/test_complexity.py
new file mode 100644
index 0000000..f0d778a
--- /dev/null
+++ b/tests/test_complexity.py
@@ -0,0 +1,104 @@
+import io
+import itertools
+import math
+import re
+import string
+import unittest
+import zipfile
+
+import pytest
+import zipp
+from jaraco.functools import compose
+from more_itertools import consume
+
+from ._support import import_or_skip
+
+
+big_o = import_or_skip('big_o')
+
+
+class TestComplexity(unittest.TestCase):
+    @pytest.mark.flaky
+    def test_implied_dirs_performance(self):
+        best, others = big_o.big_o(
+            compose(consume, zipp.CompleteDirs._implied_dirs),
+            lambda size: [
+                '/'.join(string.ascii_lowercase + str(n)) for n in range(size)
+            ],
+            max_n=1000,
+            min_n=1,
+        )
+        assert best <= big_o.complexities.Linear
+
+    def make_zip_path(self, depth=1, width=1) -> zipp.Path:
+        """
+        Construct a Path with width files at every level of depth.
+        """
+        zf = zipfile.ZipFile(io.BytesIO(), mode='w')
+        pairs = itertools.product(self.make_deep_paths(depth), self.make_names(width))
+        for path, name in pairs:
+            zf.writestr(f"{path}{name}.txt", b'')
+        zf.filename = "big un.zip"
+        return zipp.Path(zf)
+
+    @classmethod
+    def make_names(cls, width, letters=string.ascii_lowercase):
+        """
+        >>> list(TestComplexity.make_names(2))
+        ['a', 'b']
+        >>> list(TestComplexity.make_names(30))
+        ['aa', 'ab', ..., 'bd']
+        """
+        # determine how many products are needed to produce width
+        n_products = math.ceil(math.log(width, len(letters)))
+        inputs = (letters,) * n_products
+        combinations = itertools.product(*inputs)
+        names = map(''.join, combinations)
+        return itertools.islice(names, width)
+
+    @classmethod
+    def make_deep_paths(cls, depth):
+        return map(cls.make_deep_path, range(depth))
+
+    @classmethod
+    def make_deep_path(cls, depth):
+        return ''.join(('d/',) * depth)
+
+    def test_baseline_regex_complexity(self):
+        best, others = big_o.big_o(
+            lambda path: re.fullmatch(r'[^/]*\\.txt', path),
+            self.make_deep_path,
+            max_n=100,
+            min_n=1,
+        )
+        assert best <= big_o.complexities.Constant
+
+    @pytest.mark.flaky
+    def test_glob_depth(self):
+        best, others = big_o.big_o(
+            lambda path: consume(path.glob('*.txt')),
+            self.make_zip_path,
+            max_n=100,
+            min_n=1,
+        )
+        assert best <= big_o.complexities.Quadratic
+
+    @pytest.mark.flaky
+    def test_glob_width(self):
+        best, others = big_o.big_o(
+            lambda path: consume(path.glob('*.txt')),
+            lambda size: self.make_zip_path(width=size),
+            max_n=100,
+            min_n=1,
+        )
+        assert best <= big_o.complexities.Linear
+
+    @pytest.mark.flaky
+    def test_glob_width_and_depth(self):
+        best, others = big_o.big_o(
+            lambda path: consume(path.glob('*.txt')),
+            lambda size: self.make_zip_path(depth=size, width=size),
+            max_n=10,
+            min_n=1,
+        )
+        assert best <= big_o.complexities.Linear
diff --git a/tests/test_zipp.py b/tests/test_zipp.py
new file mode 100644
index 0000000..b095a06
--- /dev/null
+++ b/tests/test_zipp.py
@@ -0,0 +1,585 @@
+import io
+import itertools
+import contextlib
+import pathlib
+import tempfile
+import shutil
+import pickle
+import sys
+import unittest
+import zipfile
+
+import jaraco.itertools
+from jaraco.functools import compose
+
+import zipp
+
+from ._test_params import parameterize, Invoked
+
+
+def add_dirs(zf):
+    """
+    Given a writable zip file zf, inject directory entries for
+    any directories implied by the presence of children.
+    """
+    for name in zipp.CompleteDirs._implied_dirs(zf.namelist()):
+        zf.writestr(name, b"")
+    return zf
+
+
+def build_alpharep_fixture():
+    """
+    Create a zip file with this structure:
+
+    .
+    ├── a.txt
+    ├── b
+    │   ├── c.txt
+    │   ├── d
+    │   │   └── e.txt
+    │   └── f.txt
+    ├── g
+    │   └── h
+    │       └── i.txt
+    └── j
+        ├── k.bin
+        ├── l.baz
+        └── m.bar
+
+    This fixture has the following key characteristics:
+
+    - a file at the root (a)
+    - a file two levels deep (b/d/e)
+    - multiple files in a directory (b/c, b/f)
+    - a directory containing only a directory (g/h)
+    - a directory with files of different extensions (j/klm)
+
+    "alpha" because it uses alphabet
+    "rep" because it's a representative example
+    """
+    data = io.BytesIO()
+    zf = zipfile.ZipFile(data, "w")
+    zf.writestr("a.txt", b"content of a")
+    zf.writestr("b/c.txt", b"content of c")
+    zf.writestr("b/d/e.txt", b"content of e")
+    zf.writestr("b/f.txt", b"content of f")
+    zf.writestr("g/h/i.txt", b"content of i")
+    zf.writestr("j/k.bin", b"content of k")
+    zf.writestr("j/l.baz", b"content of l")
+    zf.writestr("j/m.bar", b"content of m")
+    zf.filename = "alpharep.zip"
+    return zf
+
+
+@contextlib.contextmanager
+def temp_dir():
+    tmpdir = tempfile.mkdtemp()
+    try:
+        yield pathlib.Path(tmpdir)
+    finally:
+        shutil.rmtree(tmpdir)
+
+
+alpharep_generators = [
+    Invoked.wrap(build_alpharep_fixture),
+    Invoked.wrap(compose(add_dirs, build_alpharep_fixture)),
+]
+
+pass_alpharep = parameterize(['alpharep'], alpharep_generators)
+
+
+class TestPath(unittest.TestCase):
+    def setUp(self):
+        self.fixtures = contextlib.ExitStack()
+        self.addCleanup(self.fixtures.close)
+
+    def zipfile_ondisk(self, alpharep):
+        tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
+        buffer = alpharep.fp
+        alpharep.close()
+        path = tmpdir / alpharep.filename
+        with path.open("wb") as strm:
+            strm.write(buffer.getvalue())
+        return path
+
+    @pass_alpharep
+    def test_iterdir_and_types(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert root.is_dir()
+        a, b, g, j = root.iterdir()
+        assert a.is_file()
+        assert b.is_dir()
+        assert g.is_dir()
+        c, f, d = b.iterdir()
+        assert c.is_file() and f.is_file()
+        (e,) = d.iterdir()
+        assert e.is_file()
+        (h,) = g.iterdir()
+        (i,) = h.iterdir()
+        assert i.is_file()
+
+    @pass_alpharep
+    def test_is_file_missing(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert not root.joinpath('missing.txt').is_file()
+
+    @pass_alpharep
+    def test_iterdir_on_file(self, alpharep):
+        root = zipp.Path(alpharep)
+        a, b, g, j = root.iterdir()
+        with self.assertRaises(ValueError):
+            a.iterdir()
+
+    @pass_alpharep
+    def test_subdir_is_dir(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert (root / 'b').is_dir()
+        assert (root / 'b/').is_dir()
+        assert (root / 'g').is_dir()
+        assert (root / 'g/').is_dir()
+
+    @pass_alpharep
+    def test_open(self, alpharep):
+        root = zipp.Path(alpharep)
+        a, b, g, j = root.iterdir()
+        with a.open(encoding="utf-8") as strm:
+            data = strm.read()
+        self.assertEqual(data, "content of a")
+        with a.open('r', "utf-8") as strm:  # not a kw, no gh-101144 TypeError
+            data = strm.read()
+        self.assertEqual(data, "content of a")
+
+    def test_open_encoding_utf16(self):
+        in_memory_file = io.BytesIO()
+        zf = zipfile.ZipFile(in_memory_file, "w")
+        zf.writestr("path/16.txt", "This was utf-16".encode("utf-16"))
+        zf.filename = "test_open_utf16.zip"
+        root = zipp.Path(zf)
+        (path,) = root.iterdir()
+        u16 = path.joinpath("16.txt")
+        with u16.open('r', "utf-16") as strm:
+            data = strm.read()
+        assert data == "This was utf-16"
+        with u16.open(encoding="utf-16") as strm:
+            data = strm.read()
+        assert data == "This was utf-16"
+
+    def test_open_encoding_errors(self):
+        in_memory_file = io.BytesIO()
+        zf = zipfile.ZipFile(in_memory_file, "w")
+        zf.writestr("path/bad-utf8.bin", b"invalid utf-8: \xff\xff.")
+        zf.filename = "test_read_text_encoding_errors.zip"
+        root = zipp.Path(zf)
+        (path,) = root.iterdir()
+        u16 = path.joinpath("bad-utf8.bin")
+
+        # encoding= as a positional argument for gh-101144.
+        data = u16.read_text("utf-8", errors="ignore")
+        assert data == "invalid utf-8: ."
+        with u16.open("r", "utf-8", errors="surrogateescape") as f:
+            assert f.read() == "invalid utf-8: \udcff\udcff."
+
+        # encoding= both positional and keyword is an error; gh-101144.
+        with self.assertRaisesRegex(TypeError, "encoding"):
+            data = u16.read_text("utf-8", encoding="utf-8")
+
+        # both keyword arguments work.
+        with u16.open("r", encoding="utf-8", errors="strict") as f:
+            # error during decoding with wrong codec.
+            with self.assertRaises(UnicodeDecodeError):
+                f.read()
+
+    @unittest.skipIf(
+        not getattr(sys.flags, 'warn_default_encoding', 0),
+        "Requires warn_default_encoding",
+    )
+    @pass_alpharep
+    def test_encoding_warnings(self, alpharep):
+        """EncodingWarning must blame the read_text and open calls."""
+        assert sys.flags.warn_default_encoding
+        root = zipp.Path(alpharep)
+        with self.assertWarns(EncodingWarning) as wc:
+            root.joinpath("a.txt").read_text()
+        assert __file__ == wc.filename
+        with self.assertWarns(EncodingWarning) as wc:
+            root.joinpath("a.txt").open("r").close()
+        assert __file__ == wc.filename
+
+    def test_open_write(self):
+        """
+        If the zipfile is open for write, it should be possible to
+        write bytes or text to it.
+        """
+        zf = zipp.Path(zipfile.ZipFile(io.BytesIO(), mode='w'))
+        with zf.joinpath('file.bin').open('wb') as strm:
+            strm.write(b'binary contents')
+        with zf.joinpath('file.txt').open('w', encoding="utf-8") as strm:
+            strm.write('text file')
+
+    def test_open_extant_directory(self):
+        """
+        Attempting to open a directory raises IsADirectoryError.
+        """
+        zf = zipp.Path(add_dirs(build_alpharep_fixture()))
+        with self.assertRaises(IsADirectoryError):
+            zf.joinpath('b').open()
+
+    @pass_alpharep
+    def test_open_binary_invalid_args(self, alpharep):
+        root = zipp.Path(alpharep)
+        with self.assertRaises(ValueError):
+            root.joinpath('a.txt').open('rb', encoding='utf-8')
+        with self.assertRaises(ValueError):
+            root.joinpath('a.txt').open('rb', 'utf-8')
+
+    def test_open_missing_directory(self):
+        """
+        Attempting to open a missing directory raises FileNotFoundError.
+        """
+        zf = zipp.Path(add_dirs(build_alpharep_fixture()))
+        with self.assertRaises(FileNotFoundError):
+            zf.joinpath('z').open()
+
+    @pass_alpharep
+    def test_read(self, alpharep):
+        root = zipp.Path(alpharep)
+        a, b, g, j = root.iterdir()
+        assert a.read_text(encoding="utf-8") == "content of a"
+        # Also check positional encoding arg (gh-101144).
+        assert a.read_text("utf-8") == "content of a"
+        assert a.read_bytes() == b"content of a"
+
+    @pass_alpharep
+    def test_joinpath(self, alpharep):
+        root = zipp.Path(alpharep)
+        a = root.joinpath("a.txt")
+        assert a.is_file()
+        e = root.joinpath("b").joinpath("d").joinpath("e.txt")
+        assert e.read_text(encoding="utf-8") == "content of e"
+
+    @pass_alpharep
+    def test_joinpath_multiple(self, alpharep):
+        root = zipp.Path(alpharep)
+        e = root.joinpath("b", "d", "e.txt")
+        assert e.read_text(encoding="utf-8") == "content of e"
+
+    @pass_alpharep
+    def test_traverse_truediv(self, alpharep):
+        root = zipp.Path(alpharep)
+        a = root / "a.txt"
+        assert a.is_file()
+        e = root / "b" / "d" / "e.txt"
+        assert e.read_text(encoding="utf-8") == "content of e"
+
+    @pass_alpharep
+    def test_pathlike_construction(self, alpharep):
+        """
+        zipp.Path should be constructable from a path-like object
+        """
+        zipfile_ondisk = self.zipfile_ondisk(alpharep)
+        pathlike = pathlib.Path(str(zipfile_ondisk))
+        zipp.Path(pathlike)
+
+    @pass_alpharep
+    def test_traverse_pathlike(self, alpharep):
+        root = zipp.Path(alpharep)
+        root / pathlib.Path("a")
+
+    @pass_alpharep
+    def test_parent(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert (root / 'a').parent.at == ''
+        assert (root / 'a' / 'b').parent.at == 'a/'
+
+    @pass_alpharep
+    def test_dir_parent(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert (root / 'b').parent.at == ''
+        assert (root / 'b/').parent.at == ''
+
+    @pass_alpharep
+    def test_missing_dir_parent(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert (root / 'missing dir/').parent.at == ''
+
+    @pass_alpharep
+    def test_mutability(self, alpharep):
+        """
+        If the underlying zipfile is changed, the Path object should
+        reflect that change.
+        """
+        root = zipp.Path(alpharep)
+        a, b, g, j = root.iterdir()
+        alpharep.writestr('foo.txt', 'foo')
+        alpharep.writestr('bar/baz.txt', 'baz')
+        assert any(child.name == 'foo.txt' for child in root.iterdir())
+        assert (root / 'foo.txt').read_text(encoding="utf-8") == 'foo'
+        (baz,) = (root / 'bar').iterdir()
+        assert baz.read_text(encoding="utf-8") == 'baz'
+
+    HUGE_ZIPFILE_NUM_ENTRIES = 2**13
+
+    def huge_zipfile(self):
+        """Create a read-only zipfile with a huge number of entries entries."""
+        strm = io.BytesIO()
+        zf = zipfile.ZipFile(strm, "w")
+        for entry in map(str, range(self.HUGE_ZIPFILE_NUM_ENTRIES)):
+            zf.writestr(entry, entry)
+        zf.mode = 'r'
+        return zf
+
+    def test_joinpath_constant_time(self):
+        """
+        Ensure joinpath on items in zipfile is linear time.
+        """
+        root = zipp.Path(self.huge_zipfile())
+        entries = jaraco.itertools.Counter(root.iterdir())
+        for entry in entries:
+            entry.joinpath('suffix')
+        # Check the file iterated all items
+        assert entries.count == self.HUGE_ZIPFILE_NUM_ENTRIES
+
+    @pass_alpharep
+    def test_read_does_not_close(self, alpharep):
+        alpharep = self.zipfile_ondisk(alpharep)
+        with zipfile.ZipFile(alpharep) as file:
+            for rep in range(2):
+                zipp.Path(file, 'a.txt').read_text(encoding="utf-8")
+
+    @pass_alpharep
+    def test_subclass(self, alpharep):
+        class Subclass(zipp.Path):
+            pass
+
+        root = Subclass(alpharep)
+        assert isinstance(root / 'b', Subclass)
+
+    @pass_alpharep
+    def test_filename(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert root.filename == pathlib.Path('alpharep.zip')
+
+    @pass_alpharep
+    def test_root_name(self, alpharep):
+        """
+        The name of the root should be the name of the zipfile
+        """
+        root = zipp.Path(alpharep)
+        assert root.name == 'alpharep.zip' == root.filename.name
+
+    @pass_alpharep
+    def test_suffix(self, alpharep):
+        """
+        The suffix of the root should be the suffix of the zipfile.
+        The suffix of each nested file is the final component's last suffix, if any.
+        Includes the leading period, just like pathlib.Path.
+        """
+        root = zipp.Path(alpharep)
+        assert root.suffix == '.zip' == root.filename.suffix
+
+        b = root / "b.txt"
+        assert b.suffix == ".txt"
+
+        c = root / "c" / "filename.tar.gz"
+        assert c.suffix == ".gz"
+
+        d = root / "d"
+        assert d.suffix == ""
+
+    @pass_alpharep
+    def test_suffixes(self, alpharep):
+        """
+        The suffix of the root should be the suffix of the zipfile.
+        The suffix of each nested file is the final component's last suffix, if any.
+        Includes the leading period, just like pathlib.Path.
+        """
+        root = zipp.Path(alpharep)
+        assert root.suffixes == ['.zip'] == root.filename.suffixes
+
+        b = root / 'b.txt'
+        assert b.suffixes == ['.txt']
+
+        c = root / 'c' / 'filename.tar.gz'
+        assert c.suffixes == ['.tar', '.gz']
+
+        d = root / 'd'
+        assert d.suffixes == []
+
+        e = root / '.hgrc'
+        assert e.suffixes == []
+
+    @pass_alpharep
+    def test_suffix_no_filename(self, alpharep):
+        alpharep.filename = None
+        root = zipp.Path(alpharep)
+        assert root.joinpath('example').suffix == ""
+        assert root.joinpath('example').suffixes == []
+
+    @pass_alpharep
+    def test_stem(self, alpharep):
+        """
+        The final path component, without its suffix
+        """
+        root = zipp.Path(alpharep)
+        assert root.stem == 'alpharep' == root.filename.stem
+
+        b = root / "b.txt"
+        assert b.stem == "b"
+
+        c = root / "c" / "filename.tar.gz"
+        assert c.stem == "filename.tar"
+
+        d = root / "d"
+        assert d.stem == "d"
+
+        assert (root / ".gitignore").stem == ".gitignore"
+
+    @pass_alpharep
+    def test_root_parent(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert root.parent == pathlib.Path('.')
+        root.root.filename = 'foo/bar.zip'
+        assert root.parent == pathlib.Path('foo')
+
+    @pass_alpharep
+    def test_root_unnamed(self, alpharep):
+        """
+        It is an error to attempt to get the name
+        or parent of an unnamed zipfile.
+        """
+        alpharep.filename = None
+        root = zipp.Path(alpharep)
+        with self.assertRaises(TypeError):
+            root.name
+        with self.assertRaises(TypeError):
+            root.parent
+
+        # .name and .parent should still work on subs
+        sub = root / "b"
+        assert sub.name == "b"
+        assert sub.parent
+
+    @pass_alpharep
+    def test_match_and_glob(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert not root.match("*.txt")
+
+        assert list(root.glob("b/c.*")) == [zipp.Path(alpharep, "b/c.txt")]
+        assert list(root.glob("b/*.txt")) == [
+            zipp.Path(alpharep, "b/c.txt"),
+            zipp.Path(alpharep, "b/f.txt"),
+        ]
+
+    @pass_alpharep
+    def test_glob_recursive(self, alpharep):
+        root = zipp.Path(alpharep)
+        files = root.glob("**/*.txt")
+        assert all(each.match("*.txt") for each in files)
+
+        assert list(root.glob("**/*.txt")) == list(root.rglob("*.txt"))
+
+    @pass_alpharep
+    def test_glob_subdirs(self, alpharep):
+        root = zipp.Path(alpharep)
+
+        assert list(root.glob("*/i.txt")) == []
+        assert list(root.rglob("*/i.txt")) == [zipp.Path(alpharep, "g/h/i.txt")]
+
+    @pass_alpharep
+    def test_glob_does_not_overmatch_dot(self, alpharep):
+        root = zipp.Path(alpharep)
+
+        assert list(root.glob("*.xt")) == []
+
+    @pass_alpharep
+    def test_glob_single_char(self, alpharep):
+        root = zipp.Path(alpharep)
+
+        assert list(root.glob("a?txt")) == [zipp.Path(alpharep, "a.txt")]
+        assert list(root.glob("a[.]txt")) == [zipp.Path(alpharep, "a.txt")]
+        assert list(root.glob("a[?]txt")) == []
+
+    @pass_alpharep
+    def test_glob_chars(self, alpharep):
+        root = zipp.Path(alpharep)
+
+        assert list(root.glob("j/?.b[ai][nz]")) == [
+            zipp.Path(alpharep, "j/k.bin"),
+            zipp.Path(alpharep, "j/l.baz"),
+        ]
+
+    def test_glob_empty(self):
+        root = zipp.Path(zipfile.ZipFile(io.BytesIO(), 'w'))
+        with self.assertRaises(ValueError):
+            root.glob('')
+
+    @pass_alpharep
+    def test_eq_hash(self, alpharep):
+        root = zipp.Path(alpharep)
+        assert root == zipp.Path(alpharep)
+
+        assert root != (root / "a.txt")
+        assert (root / "a.txt") == (root / "a.txt")
+
+        root = zipp.Path(alpharep)
+        assert root in {root}
+
+    @pass_alpharep
+    def test_is_symlink(self, alpharep):
+        """
+        See python/cpython#82102 for symlink support beyond this object.
+        """
+
+        root = zipp.Path(alpharep)
+        assert not root.is_symlink()
+
+    @pass_alpharep
+    def test_relative_to(self, alpharep):
+        root = zipp.Path(alpharep)
+        relative = root.joinpath("b", "c.txt").relative_to(root / "b")
+        assert str(relative) == "c.txt"
+
+        relative = root.joinpath("b", "d", "e.txt").relative_to(root / "b")
+        assert str(relative) == "d/e.txt"
+
+    @pass_alpharep
+    def test_inheritance(self, alpharep):
+        cls = type('PathChild', (zipp.Path,), {})
+        file = cls(alpharep).joinpath('some dir').parent
+        assert isinstance(file, cls)
+
+    @parameterize(
+        ['alpharep', 'path_type', 'subpath'],
+        itertools.product(
+            alpharep_generators,
+            [str, pathlib.Path],
+            ['', 'b/'],
+        ),
+    )
+    def test_pickle(self, alpharep, path_type, subpath):
+        zipfile_ondisk = path_type(self.zipfile_ondisk(alpharep))
+
+        saved_1 = pickle.dumps(zipp.Path(zipfile_ondisk, at=subpath))
+        restored_1 = pickle.loads(saved_1)
+        first, *rest = restored_1.iterdir()
+        assert first.read_text(encoding='utf-8').startswith('content of ')
+
+    @pass_alpharep
+    def test_extract_orig_with_implied_dirs(self, alpharep):
+        """
+        A zip file wrapped in a Path should extract even with implied dirs.
+        """
+        source_path = self.zipfile_ondisk(alpharep)
+        zf = zipfile.ZipFile(source_path)
+        # wrap the zipfile for its side effect
+        zipp.Path(zf)
+        zf.extractall(source_path.parent)
+
+    @pass_alpharep
+    def test_getinfo_missing(self, alpharep):
+        """
+        Validate behavior of getinfo on original zipfile after wrapping.
+        """
+        zipp.Path(alpharep)
+        with self.assertRaises(KeyError):
+            alpharep.getinfo('does-not-exist')
diff --git a/tests/write-alpharep.py b/tests/write-alpharep.py
new file mode 100644
index 0000000..e8545fc
--- /dev/null
+++ b/tests/write-alpharep.py
@@ -0,0 +1,4 @@
+from . import test_zipp
+
+
+__name__ == '__main__' and test_zipp.build_alpharep_fixture().extractall('alpharep')
diff --git a/towncrier.toml b/towncrier.toml
new file mode 100644
index 0000000..6fa480e
--- /dev/null
+++ b/towncrier.toml
@@ -0,0 +1,2 @@
+[tool.towncrier]
+title_format = "{version}"
diff --git a/tox.ini b/tox.ini
index cb542c1..12f8ce0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,36 +1,47 @@
-[tox]
-envlist = python
-minversion = 3.2
-# https://github.com/jaraco/skeleton/issues/6
-tox_pip_extensions_ext_venv_update = true
-
 [testenv]
 deps =
-	setuptools>=31.0.1
+	# workaround for numpy/numpy#23808
+	numpy @ git+https://github.com/rgommers/numpy@default-to-meson ; python_version >= "3.12"
+setenv =
+	PYTHONWARNDEFAULTENCODING = 1
 commands =
-	python -m unittest discover
+	pytest {posargs}
 usedevelop = True
-extras = testing
+extras =
+	testing
 
-[testenv:build-docs]
+[testenv:docs]
 extras =
-    docs
-    testing
+	docs
+	testing
 changedir = docs
 commands =
-    python -m sphinx . {toxinidir}/build/html
+	python -m sphinx -W --keep-going . {toxinidir}/build/html
+	python -m sphinxlint
+
+[testenv:finalize]
+skip_install = True
+deps =
+	towncrier
+	jaraco.develop >= 7.23
+passenv = *
+commands =
+	python -m jaraco.develop.finalize
+
 
 [testenv:release]
 skip_install = True
 deps =
-	pep517>=0.5
-	twine>=1.13
-	path.py
+	build
+	twine>=3
+	jaraco.develop>=7.1
 passenv =
 	TWINE_PASSWORD
+	GITHUB_TOKEN
 setenv =
 	TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
 commands =
-	python -c "import path; path.Path('dist').rmtree_p()"
-	python -m pep517.build .
+	python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)"
+	python -m build
 	python -m twine upload dist/*
+	python -m jaraco.develop.create-github-release
diff --git a/zipp.egg-info/PKG-INFO b/zipp.egg-info/PKG-INFO
index 1847ed3..9ccf2b5 100644
--- a/zipp.egg-info/PKG-INFO
+++ b/zipp.egg-info/PKG-INFO
@@ -1,39 +1,85 @@
 Metadata-Version: 2.1
 Name: zipp
-Version: 1.0.0
+Version: 3.16.2
 Summary: Backport of pathlib-compatible object wrapper for zip files
 Home-page: https://github.com/jaraco/zipp
 Author: Jason R. Coombs
 Author-email: jaraco@jaraco.com
-License: UNKNOWN
-Description: .. image:: https://img.shields.io/pypi/v/zipp.svg
-           :target: https://pypi.org/project/zipp
-        
-        .. image:: https://img.shields.io/pypi/pyversions/zipp.svg
-        
-        .. image:: https://img.shields.io/travis/jaraco/zipp/master.svg
-           :target: https://travis-ci.org/jaraco/zipp
-        
-        .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-           :target: https://github.com/ambv/black
-           :alt: Code style: Black
-        
-        .. image:: https://img.shields.io/appveyor/ci/jaraco/zipp/master.svg
-           :target: https://ci.appveyor.com/project/jaraco/zipp/branch/master
-        
-        .. .. image:: https://readthedocs.org/projects/zipp/badge/?version=latest
-        ..    :target: https://zipp.readthedocs.io/en/latest/?badge=latest
-        
-        
-        A pathlib-compatible Zipfile object wrapper. A backport of the
-        `Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
-        
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Requires-Python: >=2.7
+Classifier: Programming Language :: Python :: 3 :: Only
+Requires-Python: >=3.8
 Provides-Extra: testing
 Provides-Extra: docs
+License-File: LICENSE
+
+.. image:: https://img.shields.io/pypi/v/zipp.svg
+   :target: https://pypi.org/project/zipp
+
+.. image:: https://img.shields.io/pypi/pyversions/zipp.svg
+
+.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg
+   :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22
+   :alt: tests
+
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+    :target: https://github.com/astral-sh/ruff
+    :alt: Ruff
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: Black
+
+.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest
+..    :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2023-informational
+   :target: https://blog.jaraco.com/skeleton
+
+.. image:: https://tidelift.com/badges/package/pypi/zipp
+   :target: https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=readme
+
+
+A pathlib-compatible Zipfile object wrapper. Official backport of the standard library
+`Path object <https://docs.python.org/3.8/library/zipfile.html#path-objects>`_.
+
+
+Compatibility
+=============
+
+New features are introduced in this third-party library and later merged
+into CPython. The following table indicates which versions of this library
+were contributed to different versions in the standard library:
+
+.. list-table::
+   :header-rows: 1
+
+   * - zipp
+     - stdlib
+   * - 3.15
+     - 3.12
+   * - 3.5
+     - 3.11
+   * - 3.2
+     - 3.10
+   * - 3.3 ??
+     - 3.9
+   * - 1.0
+     - 3.8
+
+
+Usage
+=====
+
+Use ``zipp.Path`` in place of ``zipfile.Path`` on any Python.
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more <https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=referral&utm_campaign=github>`_.
diff --git a/zipp.egg-info/SOURCES.txt b/zipp.egg-info/SOURCES.txt
index 845b342..e681caa 100644
--- a/zipp.egg-info/SOURCES.txt
+++ b/zipp.egg-info/SOURCES.txt
@@ -1,22 +1,33 @@
-.flake8
+.coveragerc
+.editorconfig
 .pre-commit-config.yaml
-.readthedocs.yml
-.travis.yml
-CHANGES.rst
+.readthedocs.yaml
 LICENSE
+NEWS.rst
 README.rst
-appveyor.yml
+SECURITY.md
 conftest.py
+mypy.ini
 pyproject.toml
+pytest.ini
 setup.cfg
-setup.py
-skeleton.md
-test_zipp.py
+towncrier.toml
 tox.ini
-zipp.py
+.github/FUNDING.yml
+.github/dependabot.yml
+.github/workflows/main.yml
 docs/conf.py
 docs/history.rst
 docs/index.rst
+tests/__init__.py
+tests/_support.py
+tests/_test_params.py
+tests/test_complexity.py
+tests/test_zipp.py
+tests/write-alpharep.py
+zipp/__init__.py
+zipp/glob.py
+zipp/py310compat.py
 zipp.egg-info/PKG-INFO
 zipp.egg-info/SOURCES.txt
 zipp.egg-info/dependency_links.txt
diff --git a/zipp.egg-info/requires.txt b/zipp.egg-info/requires.txt
index 52c4322..2dbadf7 100644
--- a/zipp.egg-info/requires.txt
+++ b/zipp.egg-info/requires.txt
@@ -1,11 +1,24 @@
-more_itertools
 
 [docs]
-sphinx
-jaraco.packaging>=3.2
+sphinx>=3.5
+jaraco.packaging>=9.3
 rst.linker>=1.9
+furo
+sphinx-lint
+jaraco.tidelift>=1.4
 
 [testing]
-pathlib2
-contextlib2
-unittest2
+pytest>=6
+pytest-checkdocs>=2.4
+pytest-cov
+pytest-enabler>=2.2
+pytest-ruff
+jaraco.itertools
+jaraco.functools
+more_itertools
+big-O
+pytest-ignore-flaky
+
+[testing:platform_python_implementation != "PyPy"]
+pytest-black>=0.3.7
+pytest-mypy>=0.9.1
diff --git a/zipp.py b/zipp.py
deleted file mode 100644
index 8ab7d09..0000000
--- a/zipp.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# coding: utf-8
-
-from __future__ import division
-
-import io
-import sys
-import posixpath
-import zipfile
-import functools
-import itertools
-
-import more_itertools
-
-__metaclass__ = type
-
-
-def _parents(path):
-    """
-    Given a path with elements separated by
-    posixpath.sep, generate all parents of that path.
-
-    >>> list(_parents('b/d'))
-    ['b']
-    >>> list(_parents('/b/d/'))
-    ['/b']
-    >>> list(_parents('b/d/f/'))
-    ['b/d', 'b']
-    >>> list(_parents('b'))
-    []
-    >>> list(_parents(''))
-    []
-    """
-    return itertools.islice(_ancestry(path), 1, None)
-
-
-def _ancestry(path):
-    """
-    Given a path with elements separated by
-    posixpath.sep, generate all elements of that path
-
-    >>> list(_ancestry('b/d'))
-    ['b/d', 'b']
-    >>> list(_ancestry('/b/d/'))
-    ['/b/d', '/b']
-    >>> list(_ancestry('b/d/f/'))
-    ['b/d/f', 'b/d', 'b']
-    >>> list(_ancestry('b'))
-    ['b']
-    >>> list(_ancestry(''))
-    []
-    """
-    path = path.rstrip(posixpath.sep)
-    while path and path != posixpath.sep:
-        yield path
-        path, tail = posixpath.split(path)
-
-
-class Path:
-    """
-    A pathlib-compatible interface for zip files.
-
-    Consider a zip file with this structure::
-
-        .
-        ├── a.txt
-        └── b
-            ├── c.txt
-            └── d
-                └── e.txt
-
-    >>> data = io.BytesIO()
-    >>> zf = zipfile.ZipFile(data, 'w')
-    >>> zf.writestr('a.txt', 'content of a')
-    >>> zf.writestr('b/c.txt', 'content of c')
-    >>> zf.writestr('b/d/e.txt', 'content of e')
-    >>> zf.filename = 'abcde.zip'
-
-    Path accepts the zipfile object itself or a filename
-
-    >>> root = Path(zf)
-
-    From there, several path operations are available.
-
-    Directory iteration (including the zip file itself):
-
-    >>> a, b = root.iterdir()
-    >>> a
-    Path('abcde.zip', 'a.txt')
-    >>> b
-    Path('abcde.zip', 'b/')
-
-    name property:
-
-    >>> b.name
-    'b'
-
-    join with divide operator:
-
-    >>> c = b / 'c.txt'
-    >>> c
-    Path('abcde.zip', 'b/c.txt')
-    >>> c.name
-    'c.txt'
-
-    Read text:
-
-    >>> c.read_text()
-    'content of c'
-
-    existence:
-
-    >>> c.exists()
-    True
-    >>> (b / 'missing.txt').exists()
-    False
-
-    Coercion to string:
-
-    >>> str(c)
-    'abcde.zip/b/c.txt'
-    """
-
-    __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
-
-    def __init__(self, root, at=""):
-        self.root = (
-            root
-            if isinstance(root, zipfile.ZipFile)
-            else zipfile.ZipFile(self._pathlib_compat(root))
-        )
-        self.at = at
-
-    @staticmethod
-    def _pathlib_compat(path):
-        """
-        For path-like objects, convert to a filename for compatibility
-        on Python 3.6.1 and earlier.
-        """
-        try:
-            return path.__fspath__()
-        except AttributeError:
-            return str(path)
-
-    @property
-    def open(self):
-        return functools.partial(self.root.open, self.at)
-
-    @property
-    def name(self):
-        return posixpath.basename(self.at.rstrip("/"))
-
-    def read_text(self, *args, **kwargs):
-        with self.open() as strm:
-            return io.TextIOWrapper(strm, *args, **kwargs).read()
-
-    def read_bytes(self):
-        with self.open() as strm:
-            return strm.read()
-
-    def _is_child(self, path):
-        return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
-
-    def _next(self, at):
-        return Path(self.root, at)
-
-    def is_dir(self):
-        return not self.at or self.at.endswith("/")
-
-    def is_file(self):
-        return not self.is_dir()
-
-    def exists(self):
-        return self.at in self._names()
-
-    def iterdir(self):
-        if not self.is_dir():
-            raise ValueError("Can't listdir a file")
-        subs = map(self._next, self._names())
-        return filter(self._is_child, subs)
-
-    def __str__(self):
-        return posixpath.join(self.root.filename, self.at)
-
-    def __repr__(self):
-        return self.__repr.format(self=self)
-
-    def joinpath(self, add):
-        add = self._pathlib_compat(add)
-        next = posixpath.join(self.at, add)
-        next_dir = posixpath.join(self.at, add, "")
-        names = self._names()
-        return self._next(next_dir if next not in names and next_dir in names else next)
-
-    __truediv__ = joinpath
-
-    @staticmethod
-    def _implied_dirs(names):
-        return more_itertools.unique_everseen(
-            parent + "/"
-            for name in names
-            for parent in _parents(name)
-            if parent + "/" not in names
-        )
-
-    @classmethod
-    def _add_implied_dirs(cls, names):
-        return names + list(cls._implied_dirs(names))
-
-    @property
-    def parent(self):
-        parent_at = posixpath.dirname(self.at.rstrip('/'))
-        if parent_at:
-            parent_at += '/'
-        return self._next(parent_at)
-
-    def _names(self):
-        return self._add_implied_dirs(self.root.namelist())
-
-    if sys.version_info < (3,):
-        __div__ = __truediv__
diff --git a/zipp/__init__.py b/zipp/__init__.py
new file mode 100644
index 0000000..3354c2b
--- /dev/null
+++ b/zipp/__init__.py
@@ -0,0 +1,396 @@
+import io
+import posixpath
+import zipfile
+import itertools
+import contextlib
+import pathlib
+import re
+
+from .py310compat import text_encoding
+from .glob import translate
+
+
+__all__ = ['Path']
+
+
+def _parents(path):
+    """
+    Given a path with elements separated by
+    posixpath.sep, generate all parents of that path.
+
+    >>> list(_parents('b/d'))
+    ['b']
+    >>> list(_parents('/b/d/'))
+    ['/b']
+    >>> list(_parents('b/d/f/'))
+    ['b/d', 'b']
+    >>> list(_parents('b'))
+    []
+    >>> list(_parents(''))
+    []
+    """
+    return itertools.islice(_ancestry(path), 1, None)
+
+
+def _ancestry(path):
+    """
+    Given a path with elements separated by
+    posixpath.sep, generate all elements of that path
+
+    >>> list(_ancestry('b/d'))
+    ['b/d', 'b']
+    >>> list(_ancestry('/b/d/'))
+    ['/b/d', '/b']
+    >>> list(_ancestry('b/d/f/'))
+    ['b/d/f', 'b/d', 'b']
+    >>> list(_ancestry('b'))
+    ['b']
+    >>> list(_ancestry(''))
+    []
+    """
+    path = path.rstrip(posixpath.sep)
+    while path and path != posixpath.sep:
+        yield path
+        path, tail = posixpath.split(path)
+
+
+_dedupe = dict.fromkeys
+"""Deduplicate an iterable in original order"""
+
+
+def _difference(minuend, subtrahend):
+    """
+    Return items in minuend not in subtrahend, retaining order
+    with O(1) lookup.
+    """
+    return itertools.filterfalse(set(subtrahend).__contains__, minuend)
+
+
+class InitializedState:
+    """
+    Mix-in to save the initialization state for pickling.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.__args = args
+        self.__kwargs = kwargs
+        super().__init__(*args, **kwargs)
+
+    def __getstate__(self):
+        return self.__args, self.__kwargs
+
+    def __setstate__(self, state):
+        args, kwargs = state
+        super().__init__(*args, **kwargs)
+
+
+class CompleteDirs(InitializedState, zipfile.ZipFile):
+    """
+    A ZipFile subclass that ensures that implied directories
+    are always included in the namelist.
+
+    >>> list(CompleteDirs._implied_dirs(['foo/bar.txt', 'foo/bar/baz.txt']))
+    ['foo/', 'foo/bar/']
+    >>> list(CompleteDirs._implied_dirs(['foo/bar.txt', 'foo/bar/baz.txt', 'foo/bar/']))
+    ['foo/']
+    """
+
+    @staticmethod
+    def _implied_dirs(names):
+        parents = itertools.chain.from_iterable(map(_parents, names))
+        as_dirs = (p + posixpath.sep for p in parents)
+        return _dedupe(_difference(as_dirs, names))
+
+    def namelist(self):
+        names = super().namelist()
+        return names + list(self._implied_dirs(names))
+
+    def _name_set(self):
+        return set(self.namelist())
+
+    def resolve_dir(self, name):
+        """
+        If the name represents a directory, return that name
+        as a directory (with the trailing slash).
+        """
+        names = self._name_set()
+        dirname = name + '/'
+        dir_match = name not in names and dirname in names
+        return dirname if dir_match else name
+
+    def getinfo(self, name):
+        """
+        Supplement getinfo for implied dirs.
+        """
+        try:
+            return super().getinfo(name)
+        except KeyError:
+            if not name.endswith('/') or name not in self._name_set():
+                raise
+            return zipfile.ZipInfo(filename=name)
+
+    @classmethod
+    def make(cls, source):
+        """
+        Given a source (filename or zipfile), return an
+        appropriate CompleteDirs subclass.
+        """
+        if isinstance(source, CompleteDirs):
+            return source
+
+        if not isinstance(source, zipfile.ZipFile):
+            return cls(source)
+
+        # Only allow for FastLookup when supplied zipfile is read-only
+        if 'r' not in source.mode:
+            cls = CompleteDirs
+
+        source.__class__ = cls
+        return source
+
+
+class FastLookup(CompleteDirs):
+    """
+    ZipFile subclass to ensure implicit
+    dirs exist and are resolved rapidly.
+    """
+
+    def namelist(self):
+        with contextlib.suppress(AttributeError):
+            return self.__names
+        self.__names = super().namelist()
+        return self.__names
+
+    def _name_set(self):
+        with contextlib.suppress(AttributeError):
+            return self.__lookup
+        self.__lookup = super()._name_set()
+        return self.__lookup
+
+
+def _extract_text_encoding(encoding=None, *args, **kwargs):
+    # stacklevel=3 so that the caller of the caller see any warning.
+    return text_encoding(encoding, 3), args, kwargs
+
+
+class Path:
+    """
+    A pathlib-compatible interface for zip files.
+
+    Consider a zip file with this structure::
+
+        .
+        ├── a.txt
+        └── b
+            ├── c.txt
+            └── d
+                └── e.txt
+
+    >>> data = io.BytesIO()
+    >>> zf = zipfile.ZipFile(data, 'w')
+    >>> zf.writestr('a.txt', 'content of a')
+    >>> zf.writestr('b/c.txt', 'content of c')
+    >>> zf.writestr('b/d/e.txt', 'content of e')
+    >>> zf.filename = 'mem/abcde.zip'
+
+    Path accepts the zipfile object itself or a filename
+
+    >>> root = Path(zf)
+
+    From there, several path operations are available.
+
+    Directory iteration (including the zip file itself):
+
+    >>> a, b = root.iterdir()
+    >>> a
+    Path('mem/abcde.zip', 'a.txt')
+    >>> b
+    Path('mem/abcde.zip', 'b/')
+
+    name property:
+
+    >>> b.name
+    'b'
+
+    join with divide operator:
+
+    >>> c = b / 'c.txt'
+    >>> c
+    Path('mem/abcde.zip', 'b/c.txt')
+    >>> c.name
+    'c.txt'
+
+    Read text:
+
+    >>> c.read_text(encoding='utf-8')
+    'content of c'
+
+    existence:
+
+    >>> c.exists()
+    True
+    >>> (b / 'missing.txt').exists()
+    False
+
+    Coercion to string:
+
+    >>> import os
+    >>> str(c).replace(os.sep, posixpath.sep)
+    'mem/abcde.zip/b/c.txt'
+
+    At the root, ``name``, ``filename``, and ``parent``
+    resolve to the zipfile. Note these attributes are not
+    valid and will raise a ``ValueError`` if the zipfile
+    has no filename.
+
+    >>> root.name
+    'abcde.zip'
+    >>> str(root.filename).replace(os.sep, posixpath.sep)
+    'mem/abcde.zip'
+    >>> str(root.parent)
+    'mem'
+    """
+
+    __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
+
+    def __init__(self, root, at=""):
+        """
+        Construct a Path from a ZipFile or filename.
+
+        Note: When the source is an existing ZipFile object,
+        its type (__class__) will be mutated to a
+        specialized type. If the caller wishes to retain the
+        original type, the caller should either create a
+        separate ZipFile object or pass a filename.
+        """
+        self.root = FastLookup.make(root)
+        self.at = at
+
+    def __eq__(self, other):
+        """
+        >>> Path(zipfile.ZipFile(io.BytesIO(), 'w')) == 'foo'
+        False
+        """
+        if self.__class__ is not other.__class__:
+            return NotImplemented
+        return (self.root, self.at) == (other.root, other.at)
+
+    def __hash__(self):
+        return hash((self.root, self.at))
+
+    def open(self, mode='r', *args, pwd=None, **kwargs):
+        """
+        Open this entry as text or binary following the semantics
+        of ``pathlib.Path.open()`` by passing arguments through
+        to io.TextIOWrapper().
+        """
+        if self.is_dir():
+            raise IsADirectoryError(self)
+        zip_mode = mode[0]
+        if not self.exists() and zip_mode == 'r':
+            raise FileNotFoundError(self)
+        stream = self.root.open(self.at, zip_mode, pwd=pwd)
+        if 'b' in mode:
+            if args or kwargs:
+                raise ValueError("encoding args invalid for binary operation")
+            return stream
+        # Text mode:
+        encoding, args, kwargs = _extract_text_encoding(*args, **kwargs)
+        return io.TextIOWrapper(stream, encoding, *args, **kwargs)
+
+    def _base(self):
+        return pathlib.PurePosixPath(self.at or self.root.filename)
+
+    @property
+    def name(self):
+        return self._base().name
+
+    @property
+    def suffix(self):
+        return self._base().suffix
+
+    @property
+    def suffixes(self):
+        return self._base().suffixes
+
+    @property
+    def stem(self):
+        return self._base().stem
+
+    @property
+    def filename(self):
+        return pathlib.Path(self.root.filename).joinpath(self.at)
+
+    def read_text(self, *args, **kwargs):
+        encoding, args, kwargs = _extract_text_encoding(*args, **kwargs)
+        with self.open('r', encoding, *args, **kwargs) as strm:
+            return strm.read()
+
+    def read_bytes(self):
+        with self.open('rb') as strm:
+            return strm.read()
+
+    def _is_child(self, path):
+        return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
+
+    def _next(self, at):
+        return self.__class__(self.root, at)
+
+    def is_dir(self):
+        return not self.at or self.at.endswith("/")
+
+    def is_file(self):
+        return self.exists() and not self.is_dir()
+
+    def exists(self):
+        return self.at in self.root._name_set()
+
+    def iterdir(self):
+        if not self.is_dir():
+            raise ValueError("Can't listdir a file")
+        subs = map(self._next, self.root.namelist())
+        return filter(self._is_child, subs)
+
+    def match(self, path_pattern):
+        return pathlib.PurePosixPath(self.at).match(path_pattern)
+
+    def is_symlink(self):
+        """
+        Return whether this path is a symlink. Always false (python/cpython#82102).
+        """
+        return False
+
+    def glob(self, pattern):
+        if not pattern:
+            raise ValueError(f"Unacceptable pattern: {pattern!r}")
+
+        prefix = re.escape(self.at)
+        matches = re.compile(prefix + translate(pattern)).fullmatch
+        return map(self._next, filter(matches, self.root.namelist()))
+
+    def rglob(self, pattern):
+        return self.glob(f'**/{pattern}')
+
+    def relative_to(self, other, *extra):
+        return posixpath.relpath(str(self), str(other.joinpath(*extra)))
+
+    def __str__(self):
+        return posixpath.join(self.root.filename, self.at)
+
+    def __repr__(self):
+        return self.__repr.format(self=self)
+
+    def joinpath(self, *other):
+        next = posixpath.join(self.at, *other)
+        return self._next(self.root.resolve_dir(next))
+
+    __truediv__ = joinpath
+
+    @property
+    def parent(self):
+        if not self.at:
+            return self.filename.parent
+        parent_at = posixpath.dirname(self.at.rstrip('/'))
+        if parent_at:
+            parent_at += '/'
+        return self._next(parent_at)
diff --git a/zipp/glob.py b/zipp/glob.py
new file mode 100644
index 0000000..4a2e665
--- /dev/null
+++ b/zipp/glob.py
@@ -0,0 +1,40 @@
+import re
+
+
+def translate(pattern):
+    r"""
+    Given a glob pattern, produce a regex that matches it.
+
+    >>> translate('*.txt')
+    '[^/]*\\.txt'
+    >>> translate('a?txt')
+    'a.txt'
+    >>> translate('**/*')
+    '.*/[^/]*'
+    """
+    return ''.join(map(replace, separate(pattern)))
+
+
+def separate(pattern):
+    """
+    Separate out character sets to avoid translating their contents.
+
+    >>> [m.group(0) for m in separate('*.txt')]
+    ['*.txt']
+    >>> [m.group(0) for m in separate('a[?]txt')]
+    ['a', '[?]', 'txt']
+    """
+    return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern)
+
+
+def replace(match):
+    """
+    Perform the replacements for a match from :func:`separate`.
+    """
+
+    return match.group('set') or (
+        re.escape(match.group(0))
+        .replace('\\*\\*', r'.*')
+        .replace('\\*', r'[^/]*')
+        .replace('\\?', r'.')
+    )
diff --git a/zipp/py310compat.py b/zipp/py310compat.py
new file mode 100644
index 0000000..d5ca53e
--- /dev/null
+++ b/zipp/py310compat.py
@@ -0,0 +1,11 @@
+import sys
+import io
+
+
+def _text_encoding(encoding, stacklevel=2, /):  # pragma: no cover
+    return encoding
+
+
+text_encoding = (
+    io.text_encoding if sys.version_info > (3, 10) else _text_encoding  # type: ignore
+)

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-3.16.2.dist-info/INSTALLER
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-3.16.2.dist-info/METADATA
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-3.16.2.dist-info/WHEEL
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-3.16.2.dist-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp/__init__.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp/glob.py
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp/py310compat.py

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-1.0.0.dist-info/METADATA
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-1.0.0.dist-info/RECORD
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-1.0.0.dist-info/WHEEL
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp-1.0.0.dist-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/zipp.py

Control files: lines which differ (wdiff format)

  • Depends: python3-more-itertools, python3:any

More details

Full run details