diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 642b821..0f18202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,42 +2,80 @@ name: Python package on: push: + branches: ["**"] pull_request: - branches: - - main + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - tests: - runs-on: ubuntu-latest + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: 0.12.24 + - name: Build sdist and wheel + run: uv run --python 3.14 ./run build --verbose + - name: Upload packages + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist + path: dist/ + + test: + needs: build + runs-on: ubuntu-24.04 strategy: matrix: python-version: ["3.8", "3.9", "3.10"] + fail-fast: false steps: - - uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Download packages + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - python-version: ${{ matrix.python-version }} + name: dist + path: dist/ - name: Install uv - uses: astral-sh/setup-uv@v10 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: 0.12.24 - - name: Run tests - run: uv run ./run tests + cache-suffix: ${{ matrix.python-version }} + - name: Install package and run tests + run: | + echo "$ uv venv --python ${{ matrix.python-version }} .venv" + uv venv --python ${{ matrix.python-version }} .venv + echo "" + echo "$ source .venv/bin/activate" + source .venv/bin/activate + echo "" + echo "$ uv pip install dist/*.whl" + uv pip install dist/*.whl + echo "" + echo "$ ./run tests --verbose" + ./run tests --verbose presubmit: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] - - uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install uv - uses: astral-sh/setup-uv@v10 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: 0.12.24 + cache-suffix: ${{ matrix.python-version }} - name: Run presubmit - run: uv run ./run presubmit + run: | + uv run --python ${{ matrix.python-version }} ./run presubmit --verbose diff --git a/README.md b/README.md index cea31a7..6afcaa6 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,22 @@ be legal object keys in JavaScript, but they aren't in JSON5. keyword *is* supported, though, and might be able to serve as a workaround. -## Contributing +## Contributing: -`json5` has no runtime dependencies and it is supported on Python version 3.8 -or later. However, in order to develop and build the package you need a -bunch of extra tools and the latest versions of those tools may require 3.9 -or later. You can install the extra environment on 3.8 (and get older versions -of the tools), but they may not run completely cleanly. +`json5` has no runtime dependencies and it is supported on Python version +**3.8** or later. -#### On Mac +However, in order to develop, test, build and publish the package you need a +bunch of extra tools that are only supported on **3.11** or newer; on older +versions, you can run the test, but not do anything else. + +When changing the code, do not add anything that only runs on 3.9 or later +versions, and do not rely on `from __future__ import annotations`. + +Use a 79-character line and prefer single quotes to double; otherwise follow +PEP-8 and the normal `ruff format` coding style. + +### On Mac The easiest thing to do is to install [`uv`](https://docs.astral.sh/uv) and use `uv` and the `//run` script to develop things. See `./run --help` for @@ -85,42 +92,105 @@ $ cd pyjson5 $ source $(./run devenv) # To activate a venv w/ all the needed dev tools. ``` -#### On other platforms +### On other platforms Install `uv` via whatever mechanism is appropriate. -### Create the venv - -``` -$ ./run devenv -``` +### Creating a venv -(This calls `uv sync --extra dev`.) - -### Running the tests +You do not have to create a venv to do anything, if you use the `run` +script, but if you want to invoke tools like `ruff` directly, you +can do so as follows: ``` -$ ./run tests +$ ./run devenv # which really just calls `uv sync`. +$ source .venv/bin/activate ``` -### Updating the packages +### Run tests and other checks ``` -# Update the version in json5/version.py to $VERSION, which should be of -# the form X.Y.Z where X, Y, and Z are numbers. -$ ./run regen $ ./run presubmit -$ git commit -a -m "Bump the version to $VERSION" -$ git tag "v$VERSION" -$ ./run build -$ ./run publish --prod -$ git push origin -$ git push --tags origin ``` +`//run` supports a number of other commands: + +* `regen` will regenerate the parser when the grammar is updated; in order + to run this you need `glop` to be available as described above. + +* `format` will format all the code using `ruff format` for the Python code + and `pyproject-fmt` for the `pyproject.toml` file; `format --check` will + report whether the code is formatted properly without actually changing + anything. It checks for the coding style described above. + +* `checks` checks the code for problems without actually executing the code. + It runs: + * `ruff check` + * `pylint` + * `mypy` + +* `tests` runs all of the tests. Write tests using either `doctest` or + `unittest`. Aim for 100% line coverage in tests (see next bullet). + +* `coverage` runs all of the tests using the `coverage` tool in order to + collect code coverage. You can pass `-b/--branch` to collect branch + coverage. You should target 100% line coverage on any changes, as there + shouldn't be a reason you can't do this, and actually ensuring that you + execute every line can catch problems in Python that might otherwise be + caught earlier in statically-typed languages (though the `checks` go + a long way to accomplishing the same thing). + +* `build` builds the package to get it ready to publish + +* `publish` tries to publish the package. You must specify `--dev` or `--prod` + to indicate which PyPI repository you want to publish to; explicitly having + to mention one or the other helps ensure that you don't try to publish + something by accident. You can run `publish --check` in order to ensure + that the package is built correctly without actually trying to publish it. + You can only actually publish the package if you have permission to do so, + of course. + +`presubmit` just runs all of the above commands at once (though not `regen` +at the moment, because you might not have `glop` installed; this'll change +when `glop` is properly packaged and installed in the venv. + +Other `run` commands: + +* `devenv` creates a virtual environ using `uv` and echoes the command needed +to activate it. + +* `clean` will remove any unversioned files from the repo. + +* `help` prints usage. + +## Releasing a new version: + +1. Update the version in `json5/version.py` to $VERSION, which should be + of the form X.Y.Z, where X, Y, and Z are numbers. Use + [semantic versioning](https://semver.org/) when increasing the numbers: + * X increases only when making backwards-incompatible or other very + significant changes. Try very hard to avoid this. + * Y increases when adding functionality in a backwards-compatible way. + * Z increases when you are fixing bugs in a backwards-compatible way. + + In between releases, the version *may* be set to `X.Y.Z.dev0' to indicate + that this is not an officially released version. + +2. `./run presubmit` once more and make sure everything passes. + +3. `git commit -a -m "Bump the version to $VERSION"` + +4. `git tag "v$VERSION"` + +5. `./run publish --prod` (which will build the package if need be first) + +6. `git push origin` + +7. `git push --tags origin` + (Assuming you have upload privileges to PyPI and the GitHub repo, of course.) -## Version History / Release Notes +## Version History / Release Notes: * v0.17.0dev0 (2026-10-08) * Rework the project dependencies. As before, the module still has diff --git a/run b/run index 67063bf..6ecf566 100755 --- a/run +++ b/run @@ -50,11 +50,15 @@ class Runner: sys.exit(2) if 'VIRTUAL_ENV' in os.environ: - self.run_cmd = [] + self.run_cmd = ['python', '-m'] else: - self.run_cmd = [self.uv_path, 'run'] + self.run_cmd = [self.uv_path, 'run', 'python', '-m'] def main(self, argv): + # Change stdout to be unbuffered so that output shows up in + # the right interleaving between this process and any child + # processes. + sys.stdout.reconfigure(line_buffering=True) self.parse_args(argv) self.failfast = getattr(self.args, 'failfast', False) if not self.args.quiet and ( @@ -276,9 +280,7 @@ class Runner: if not self.args.quiet: print('\n### pylint') - proc = self._call( - self.run_cmd[:-1] + ['pylint', 'run'] + self._files() - ) + proc = self._call(self.run_cmd + ['pylint', 'run'] + self._files()) if proc.returncode == 0 and not self.args.quiet: print('Success: pylint found no issues') @@ -316,7 +318,6 @@ class Runner: cmd.append('--branch') cmd.extend( [ - 'python', '-m', 'unittest', 'discover', @@ -341,7 +342,7 @@ class Runner: print('You need to have `uv` installed to set up a dev env.') return 1 - cmd = [self.uv_path, 'sync', '--extra', 'dev'] + cmd = [self.uv_path, 'sync'] if self.args.quiet: cmd.append('--quiet') @@ -353,9 +354,7 @@ class Runner: def run_format(self): self._check_python_version() - cmd = [ - self.uv_path, - 'run', + cmd = self.run_cmd[:-2] + [ 'pyproject-fmt', '--column-width', '80', @@ -416,7 +415,8 @@ class Runner: self.args.unit = False steps = ( - ('regen --check', self.run_regen), + # TODO: Enable this once glop is properly packaged and available. + # ('regen --check', self.run_regen), ('format --check', self.run_format), ('checks', self.run_checks), ('tests', self.run_tests), @@ -431,7 +431,7 @@ class Runner: if not self.args.quiet and not self.returncode: print('\n## Presubmit passed.') - else: + elif self.returncode: print('\n## Presubmit failed.') return self.returncode @@ -443,7 +443,6 @@ class Runner: return 2 self._check_version() - import pdb; pdb.set_trace() sep = os.path.sep tgz = f'dist{sep}{self.package}-{self.version}.tar.gz' @@ -535,13 +534,9 @@ class Runner: return proc.returncode def run_tests(self): - _ = self._call( - self.run_cmd + ['python', '-m', 'doctest', 'json5/lib.py'] - ) + _ = self._call(self.run_cmd + ['doctest', 'json5/lib.py']) cmd = self.run_cmd + [ - 'python', - '-m', 'unittest', 'discover', '-p',