Skip to content

Commit

Permalink
typing
Browse files Browse the repository at this point in the history
Add mypy to tox config, include json5-tests suite, reorder imports, pre-commit config, black formatting
  • Loading branch information
spyoungtech committed Aug 1, 2023
1 parent 6f61d11 commit 9761474
Show file tree
Hide file tree
Showing 22 changed files with 489 additions and 221 deletions.
1 change: 1 addition & 0 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
python -m pip install -r requirements-dev.txt
python -m pip install .
python -m pip install tox
git clone https://github.com/json5/json5-tests.git
- name: Test with coverage/pytest
env:
Expand Down
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,16 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.10.0
hooks:
- id: reorder-python-imports

- repo: https://github.com/psf/black
rev: '23.7.0'
hooks:
- id: black
args:
- "-S"
- "-l"
- "120"
7 changes: 1 addition & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))


# -- Project information -----------------------------------------------------

project = 'json-five'
Expand All @@ -30,8 +26,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
extensions = []

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
Expand Down
6 changes: 4 additions & 2 deletions json5/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .loader import loads, load
from .dumper import dumps, dump
from .dumper import dump
from .dumper import dumps
from .loader import load
from .loader import loads
from .utils import JSON5DecodeError
28 changes: 18 additions & 10 deletions json5/dumper.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
from __future__ import annotations
from typing import Dict, Any, Optional, List, Union

import io
import json
import math
import typing
from abc import abstractmethod
import math
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Union

from .loader import JsonIdentifier
from .utils import singledispatchmethod
from json5.model import *
from collections import UserDict
import json
import io


class Environment:
def __init__(self) -> None:
self.outfile: typing.TextIO = io.StringIO()
self.indent_level: int = 0
self.indent: int = 0


def write(self, s: str, indent: Optional[int] = None) -> None:
if indent is None:
indent = self.indent_level
Expand All @@ -40,6 +45,7 @@ def dumps(obj: Any, dumper: Optional[BaseDumper] = None, indent: int = 0) -> str
ret: str = dumper.env.outfile.read()
return ret


class BaseDumper:
def __init__(self, env: Optional[Environment] = None):
if env is None:
Expand All @@ -51,10 +57,12 @@ def __init__(self, env: Optional[Environment] = None):
def dump(self, obj: Any) -> Any:
return NotImplemented


class DefaultDumper(BaseDumper):
"""
Dump Python objects to a JSON string
"""

@singledispatchmethod
def dump(self, obj: Any) -> Any:
raise NotImplementedError(f"Cannot dump node {repr(obj)}")
Expand Down Expand Up @@ -88,7 +96,6 @@ def dict_to_json(self, d: Dict[Any, Any]) -> Any:
else:
self.env.write('}', indent=0)


@to_json(int)
def int_to_json(self, i: int) -> Any:
self.env.write(str(i), indent=0)
Expand All @@ -101,7 +108,6 @@ def identifier_to_json(self, s: JsonIdentifier) -> Any:
def str_to_json(self, s: str) -> Any:
self.env.write(json.dumps(s), indent=0)


@to_json(list)
def list_to_json(self, l: List[Any]) -> Any:
self.env.write('[', indent=0)
Expand All @@ -124,7 +130,6 @@ def list_to_json(self, l: List[Any]) -> Any:
self.env.indent_level -= 1
self.env.write(']')


@to_json(float)
def float_to_json(self, f: float) -> Any:
if f == math.inf:
Expand All @@ -144,10 +149,12 @@ def bool_to_json(self, b: bool) -> Any:
def none_to_json(self, _: Any) -> Any:
self.env.write('null', indent=0)


class ModelDumper:
"""
Dump a model to a JSON string
"""

def __init__(self, env: Optional[Environment] = None):
# any provided environment is ignored
self.env = Environment()
Expand Down Expand Up @@ -301,10 +308,12 @@ def nan_to_json(self, node: NaN) -> Any:
self.env.write('NaN')
self.process_wsc_after(node)


class Modelizer:
"""
Turn Python objects into a model
"""

@singledispatchmethod
def modelize(self, obj: Any) -> Node:
raise NotImplementedError(f"Cannot modelize object of type {type(obj)}")
Expand Down Expand Up @@ -337,7 +346,6 @@ def list_to_model(self, lst: List[Any]) -> JSONArray:
def int_to_model(self, i: int) -> Integer:
return Integer(str(i))


@to_model(float)
def float_to_model(self, f: float) -> Union[Infinity, NaN, Float, UnaryOp]:
if f == math.inf:
Expand Down
70 changes: 45 additions & 25 deletions json5/loader.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
from __future__ import annotations
import types

import sys
import logging
import typing
from abc import abstractmethod
from typing import Dict, List, Callable, Literal, Tuple
from json5.parser import parse_source
from typing import Callable
from typing import Dict
from typing import List
from typing import Literal
from typing import Tuple

from json5.model import *
from json5.parser import parse_source
from json5.utils import singledispatchmethod
from collections import UserString


import logging
logger = logging.getLogger(__name__)
# logger.setLevel(level=logging.DEBUG)
# logger.addHandler(logging.StreamHandler(stream=sys.stderr))


class Environment:
def __init__(self,
object_hook: Optional[Callable[[Dict[typing.Any, typing.Any]], typing.Any]] = None,
parse_float: Optional[Callable[[str], typing.Any]] = None,
parse_int: Optional[Callable[[str], typing.Any]] = None,
parse_constant: Optional[Callable[[Literal['-Infinity', 'Infinity', 'NaN']], typing.Any]] = None,
strict: bool = True,
object_pairs_hook: Optional[Callable[[List[Tuple[Union[str, JsonIdentifier], typing.Any]]], typing.Any]] = None,
parse_json5_identifiers: Optional[Callable[[JsonIdentifier], typing.Any]] = None
):
def __init__(
self,
object_hook: Optional[Callable[[Dict[typing.Any, typing.Any]], typing.Any]] = None,
parse_float: Optional[Callable[[str], typing.Any]] = None,
parse_int: Optional[Callable[[str], typing.Any]] = None,
parse_constant: Optional[Callable[[Literal['-Infinity', 'Infinity', 'NaN']], typing.Any]] = None,
strict: bool = True,
object_pairs_hook: Optional[Callable[[List[Tuple[Union[str, JsonIdentifier], typing.Any]]], typing.Any]] = None,
parse_json5_identifiers: Optional[Callable[[JsonIdentifier], typing.Any]] = None,
):
self.object_hook: Optional[Callable[[Dict[typing.Any, typing.Any]], typing.Any]] = object_hook
self.parse_float: Optional[Callable[[str], typing.Any]] = parse_float
self.parse_int: Optional[Callable[[str], typing.Any]] = parse_int
self.parse_constant: Optional[Callable[[Literal['-Infinity', 'Infinity', 'NaN']], typing.Any]] = parse_constant
self.strict: bool = strict
self.object_pairs_hook: Optional[Callable[[List[Tuple[Union[str, JsonIdentifier], typing.Any]]], typing.Any]] = object_pairs_hook
self.object_pairs_hook: Optional[
Callable[[List[Tuple[Union[str, JsonIdentifier], typing.Any]]], typing.Any]
] = object_pairs_hook
self.parse_json5_identifiers: Optional[Callable[[JsonIdentifier], typing.Any]] = parse_json5_identifiers


class JsonIdentifier(str):
...


def load(f: typing.TextIO, **kwargs: typing.Any) -> typing.Any:
"""
Like loads, but takes a file-like object with a read method.
Expand All @@ -49,13 +55,27 @@ def load(f: typing.TextIO, **kwargs: typing.Any) -> typing.Any:
text = f.read()
return loads(text, **kwargs)


@typing.overload
def loads(s: str, *, loader: None) -> typing.Union[int, float, str, dict[typing.Any, typing.Any], list[typing.Any], None, ]: ...
def loads(
s: str, *, loader: None
) -> typing.Union[int, float, str, dict[typing.Any, typing.Any], list[typing.Any], None,]:
...


@typing.overload
def loads(s: str, *, loader: DefaultLoader) -> typing.Union[int, float, str, dict[typing.Any, typing.Any], list[typing.Any], None, ]: ...
def loads(
s: str, *, loader: DefaultLoader
) -> typing.Union[int, float, str, dict[typing.Any, typing.Any], list[typing.Any], None,]:
...


@typing.overload
def loads(s: str, *, loader: typing.Optional[LoaderBase]=None, **kwargs: typing.Any) -> typing.Any: ...
def loads(s: str, *, loader: typing.Optional[LoaderBase]=None, **kwargs: typing.Any) -> typing.Any:
def loads(s: str, *, loader: typing.Optional[LoaderBase] = None, **kwargs: typing.Any) -> typing.Any:
...


def loads(s: str, *, loader: typing.Optional[LoaderBase] = None, **kwargs: typing.Any) -> typing.Any:
"""
Take a string of JSON text and deserialize it
Expand All @@ -75,8 +95,9 @@ def loads(s: str, *, loader: typing.Optional[LoaderBase]=None, **kwargs: typing.
loader = DefaultLoader(**kwargs)
return loader.load(model)


class LoaderBase:
def __init__(self, env: typing.Optional[Environment]=None, **env_kwargs: typing.Any):
def __init__(self, env: typing.Optional[Environment] = None, **env_kwargs: typing.Any):
if env is None:
env = Environment(**env_kwargs)
self.env: Environment = env
Expand All @@ -86,6 +107,7 @@ def __init__(self, env: typing.Optional[Environment]=None, **env_kwargs: typing.
def load(self, node: Node) -> typing.Any:
return NotImplemented


class DefaultLoader(LoaderBase):
@singledispatchmethod
def load(self, node: Node) -> typing.Any:
Expand Down Expand Up @@ -113,7 +135,6 @@ def json_object_to_python(self, node: JSONObject) -> typing.Any:
else:
return d


@to_python(JSONArray)
def json_array_to_python(self, node: JSONArray) -> list[typing.Any]:
logger.debug('json_array_to_python evaluating node %r', node)
Expand Down Expand Up @@ -169,10 +190,9 @@ def unary_to_python(self, node: UnaryOp) -> typing.Any:
@to_python(String)
def string_to_python(self, node: Union[DoubleQuotedString, SingleQuotedString]) -> str:
logger.debug('string_to_python evaluating node %r', node)
ret : str = node.characters
ret: str = node.characters
return ret


@to_python(NullLiteral)
def null_to_python(self, node: NullLiteral) -> None:
logger.debug('null_to_python evaluating node %r', node)
Expand Down
Loading

0 comments on commit 9761474

Please sign in to comment.