diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e4cbad7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: Source compatibility + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + name: ${{ matrix.os }} / Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, windows-2022] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + id: python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Record interpreter + shell: python + run: | + import platform + import struct + import sys + import tempfile + from pathlib import Path + + print(sys.version) + print(platform.platform()) + print('Architecture bits:', struct.calcsize('P') * 8) + print('Temporary directory:', tempfile.gettempdir()) + print('Resolved temporary directory:', Path(tempfile.gettempdir()).resolve()) + - name: Run source tests + run: python -m unittest discover -s tests -v + - name: Check source syntax + if: ${{ !cancelled() && steps.python.outcome == 'success' }} + run: python -m compileall -q MHTMLExtractor.py mhtmlextractor diff --git a/AGENTS.md b/AGENTS.md index d52da09..9949d4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,8 @@ ## Install - Use Python 3.7 or newer. +- Source CI covers x64 CPython 3.7 through 3.14 on Windows and Linux. Keep the minimum at 3.7 and do not add an upper installation cap. +- Project compatibility testing does not imply upstream maintenance of end-of-life Python versions. - From the repository root, install normally with `python -m pip install .`. - For local development, install editable with `python -m pip install -e .`. - After installation, confirm the command surface with `mhtml-extract --help`. @@ -39,10 +41,14 @@ Use `MHTMLExtractor` or `mhtml-extract` when you need files written to disk. ## Test +- `.github/workflows/tests.yml` runs the source suite and syntax check on pushes and pull requests across Windows Server 2022 / Ubuntu 22.04 and CPython 3.7-3.14. +- Source tests use only the standard library and run without a package installation. - Run tests with `python -m unittest discover -s tests`. +- CI adds `-v` to expose skips: the TOML metadata test skips below 3.11, and symlink coverage may skip when the platform cannot create symlinks. - Run a syntax check with `python -m compileall -q MHTMLExtractor.py mhtmlextractor`. - Do not use `python -m py_compile mhtmlextractor\*.py` in PowerShell; the wildcard can be passed literally. - For installability changes, verify an editable install and run `mhtml-extract --help`. +- Source and editable-install checks do not replace separate clean wheel/sdist installation validation. ## Avoid Unsafe Assumptions diff --git a/README.md b/README.md index a96144f..39c688f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ The project uses only Python's standard library. - Python 3.7 or newer +The source compatibility CI matrix covers x64 CPython 3.7 through 3.14 on +Windows and Linux. The package has no upper Python version cap; versions beyond +this matrix are not yet verified. Compatibility testing of older Python versions +does not imply upstream maintenance. See the +[official Python version status](https://devguide.python.org/versions/). + ## Installation From the repository root: @@ -307,12 +313,24 @@ objects. ## Development -Run the test suite: +The [source compatibility workflow](.github/workflows/tests.yml) runs on pushes +and pull requests using Windows Server 2022 and Ubuntu 22.04, with x64 CPython +3.7 through 3.14. It runs the source tests and syntax check directly from the +checkout using only the standard library; no package installation is required. +Each matrix job reports its interpreter version and runs independently so one +failure does not cancel the other versions. + +Run the same test suite locally: ```bash python -m unittest discover -s tests ``` +CI adds `-v` to show individual tests and skip reasons. The TOML metadata test +skips on Python 3.7 through 3.10 because `tomllib` is available from Python 3.11; +the extraction tests still run. A symlink test may skip when the platform cannot +create symlinks. + Run a package syntax check: ```bash @@ -325,6 +343,9 @@ Check the installed command surface from a local editable install: mhtml-extract --help ``` +Source compatibility and editable-install checks do not verify installed wheel +or source-distribution behavior; those require separate clean installations. + ## License MHTMLExtractor is released under the MIT License. See `LICENSE`. diff --git a/pyproject.toml b/pyproject.toml index 9d53463..c150c3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", "Topic :: System :: Archiving", "Topic :: Utilities", diff --git a/tests/test_duplicate_parts.py b/tests/test_duplicate_parts.py index cfb3b31..f77bb94 100644 --- a/tests/test_duplicate_parts.py +++ b/tests/test_duplicate_parts.py @@ -36,7 +36,8 @@ class DuplicatePartTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory(prefix="mhtml-duplicates-") self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) + # Match the extractor's canonical paths when TEMP contains an alias. + self.root = Path(temporary.name).resolve() def archive(self, parts, name="input"): payload = [b'Content-Type: multipart/related; boundary="duplicates"\r\n\r\n'] diff --git a/tests/test_file_safety.py b/tests/test_file_safety.py index 03431c3..043efe0 100644 --- a/tests/test_file_safety.py +++ b/tests/test_file_safety.py @@ -117,7 +117,8 @@ class FileSafetyTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory(prefix="mhtml-a61-") self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) + # Match the extractor's canonical paths when TEMP contains an alias. + self.root = Path(self.temporary.name).resolve() def archive_and_output(self, name="case", **options): case = self.root / name diff --git a/tests/test_static_references.py b/tests/test_static_references.py index 866b617..3226003 100644 --- a/tests/test_static_references.py +++ b/tests/test_static_references.py @@ -53,7 +53,8 @@ class StaticReferenceTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory(prefix="mhtml-static-refs-") self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) + # Match the extractor's canonical paths when TEMP contains an alias. + self.root = Path(temporary.name).resolve() self.case_number = 0 def archive(self, parts, outer_location=None):