Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 57 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
126 changes: 98 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
31 changes: 13 additions & 18 deletions run
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -316,7 +318,6 @@ class Runner:
cmd.append('--branch')
cmd.extend(
[
'python',
'-m',
'unittest',
'discover',
Expand All @@ -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')

Expand All @@ -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',
Expand Down Expand Up @@ -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),
Expand All @@ -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

Expand All @@ -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'
Expand Down Expand Up @@ -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',
Expand Down